kash-shell 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kash/__init__.py +2 -0
- kash/__main__.py +4 -0
- kash/actions/__init__.py +55 -0
- kash/actions/core/assistant_chat.py +45 -0
- kash/actions/core/chat.py +90 -0
- kash/actions/core/format_markdown_template.py +92 -0
- kash/actions/core/markdownify.py +29 -0
- kash/actions/core/readability.py +27 -0
- kash/actions/core/show_webpage.py +28 -0
- kash/actions/core/strip_html.py +28 -0
- kash/actions/core/summarize_as_bullets.py +53 -0
- kash/actions/core/webpage_config.py +21 -0
- kash/actions/core/webpage_generate.py +29 -0
- kash/actions/meta/write_instructions.py +39 -0
- kash/actions/meta/write_new_action.py +157 -0
- kash/commands/__init__.py +21 -0
- kash/commands/base/basic_file_commands.py +183 -0
- kash/commands/base/browser_commands.py +62 -0
- kash/commands/base/debug_commands.py +214 -0
- kash/commands/base/diff_commands.py +90 -0
- kash/commands/base/files_command.py +408 -0
- kash/commands/base/general_commands.py +104 -0
- kash/commands/base/global_state_commands.py +41 -0
- kash/commands/base/logs_commands.py +92 -0
- kash/commands/base/reformat_command.py +54 -0
- kash/commands/base/search_command.py +65 -0
- kash/commands/base/show_command.py +69 -0
- kash/commands/extras/utils_commands.py +27 -0
- kash/commands/help/assistant_commands.py +97 -0
- kash/commands/help/doc_commands.py +226 -0
- kash/commands/help/help_commands.py +133 -0
- kash/commands/workspace/selection_commands.py +200 -0
- kash/commands/workspace/workspace_commands.py +640 -0
- kash/concepts/concept_formats.py +23 -0
- kash/concepts/embeddings.py +130 -0
- kash/concepts/text_similarity.py +108 -0
- kash/config/__init__.py +4 -0
- kash/config/api_keys.py +84 -0
- kash/config/capture_output.py +77 -0
- kash/config/colors.py +279 -0
- kash/config/init.py +18 -0
- kash/config/lazy_imports.py +22 -0
- kash/config/logger.py +355 -0
- kash/config/logger_basic.py +35 -0
- kash/config/logo.txt +4 -0
- kash/config/logo_fancy.txt +4 -0
- kash/config/server_config.py +51 -0
- kash/config/settings.py +196 -0
- kash/config/setup.py +51 -0
- kash/config/suppress_warnings.py +27 -0
- kash/config/text_styles.py +426 -0
- kash/docs/__init__.py +0 -0
- kash/docs/all_docs.py +58 -0
- kash/docs/load_actions_info.py +28 -0
- kash/docs/load_api_docs.py +13 -0
- kash/docs/load_help_topics.py +47 -0
- kash/docs/load_source_code.py +125 -0
- kash/docs/markdown/api_docs_template.md +42 -0
- kash/docs/markdown/assistant_instructions_template.md +114 -0
- kash/docs/markdown/readme_template.md +26 -0
- kash/docs/markdown/topics/a1_what_is_kash.md +76 -0
- kash/docs/markdown/topics/a2_progress.md +96 -0
- kash/docs/markdown/topics/a3_installation.md +119 -0
- kash/docs/markdown/topics/a4_getting_started.md +300 -0
- kash/docs/markdown/topics/a5_tips_for_use_with_other_tools.md +83 -0
- kash/docs/markdown/topics/b0_philosophy_of_kash.md +177 -0
- kash/docs/markdown/topics/b1_kash_overview.md +124 -0
- kash/docs/markdown/topics/b2_workspace_and_file_formats.md +61 -0
- kash/docs/markdown/topics/b3_modern_shell_tool_recommendations.md +83 -0
- kash/docs/markdown/topics/b4_faq.md +166 -0
- kash/docs/markdown/warning.md +7 -0
- kash/docs/markdown/welcome.md +9 -0
- kash/docs_base/docs_base.py +85 -0
- kash/docs_base/load_custom_command_info.py +27 -0
- kash/docs_base/load_faqs.py +48 -0
- kash/docs_base/load_recipe_snippets.py +48 -0
- kash/docs_base/recipes/general_system_commands.ksh +10 -0
- kash/docs_base/recipes/python_dev_commands.ksh +7 -0
- kash/docs_base/recipes/tldr_standard_commands.ksh +2144 -0
- kash/errors.py +176 -0
- kash/exec/__init__.py +16 -0
- kash/exec/action_decorators.py +412 -0
- kash/exec/action_exec.py +457 -0
- kash/exec/action_registry.py +123 -0
- kash/exec/combiners.py +127 -0
- kash/exec/command_exec.py +34 -0
- kash/exec/command_registry.py +72 -0
- kash/exec/fetch_url_metadata.py +71 -0
- kash/exec/history.py +44 -0
- kash/exec/llm_transforms.py +121 -0
- kash/exec/precondition_checks.py +71 -0
- kash/exec/precondition_registry.py +43 -0
- kash/exec/preconditions.py +152 -0
- kash/exec/resolve_args.py +123 -0
- kash/exec/shell_callable_action.py +90 -0
- kash/exec_model/__init__.py +0 -0
- kash/exec_model/args_model.py +93 -0
- kash/exec_model/commands_model.py +163 -0
- kash/exec_model/script_model.py +161 -0
- kash/exec_model/shell_model.py +21 -0
- kash/file_storage/__init__.py +0 -0
- kash/file_storage/file_store.py +642 -0
- kash/file_storage/item_file_format.py +152 -0
- kash/file_storage/metadata_dirs.py +108 -0
- kash/file_storage/mtime_cache.py +108 -0
- kash/file_storage/persisted_yaml.py +37 -0
- kash/file_storage/store_cache_warmer.py +37 -0
- kash/file_storage/store_filenames.py +53 -0
- kash/form_input/__init__.py +0 -0
- kash/form_input/prompt_input.py +44 -0
- kash/help/__init__.py +0 -0
- kash/help/assistant.py +324 -0
- kash/help/assistant_instructions.py +68 -0
- kash/help/assistant_output.py +43 -0
- kash/help/docstring_utils.py +111 -0
- kash/help/function_param_info.py +44 -0
- kash/help/help_embeddings.py +85 -0
- kash/help/help_lookups.py +60 -0
- kash/help/help_pages.py +122 -0
- kash/help/help_printing.py +169 -0
- kash/help/help_types.py +247 -0
- kash/help/recommended_commands.py +143 -0
- kash/help/tldr_help.py +296 -0
- kash/llm_utils/__init__.py +0 -0
- kash/llm_utils/chat_format.py +413 -0
- kash/llm_utils/clean_headings.py +65 -0
- kash/llm_utils/fuzzy_parsing.py +119 -0
- kash/llm_utils/language_models.py +178 -0
- kash/llm_utils/llm_completion.py +172 -0
- kash/llm_utils/llm_messages.py +36 -0
- kash/local_server/__init__.py +2 -0
- kash/local_server/local_server.py +183 -0
- kash/local_server/local_server_commands.py +55 -0
- kash/local_server/local_server_routes.py +306 -0
- kash/local_server/local_url_formatters.py +169 -0
- kash/local_server/port_tools.py +67 -0
- kash/local_server/rich_html_template.py +12 -0
- kash/mcp/__init__.py +2 -0
- kash/mcp/mcp_main.py +67 -0
- kash/mcp/mcp_server_commands.py +57 -0
- kash/mcp/mcp_server_routes.py +256 -0
- kash/mcp/mcp_server_sse.py +143 -0
- kash/mcp/mcp_server_stdio.py +45 -0
- kash/media_base/__init__.py +0 -0
- kash/media_base/audio_processing.py +27 -0
- kash/media_base/media_cache.py +178 -0
- kash/media_base/media_services.py +112 -0
- kash/media_base/media_tools.py +48 -0
- kash/media_base/services/local_file_media.py +165 -0
- kash/media_base/speech_transcription.py +224 -0
- kash/media_base/timestamp_citations.py +80 -0
- kash/model/__init__.py +73 -0
- kash/model/actions_model.py +633 -0
- kash/model/assistant_response_model.py +87 -0
- kash/model/compound_actions_model.py +188 -0
- kash/model/graph_model.py +92 -0
- kash/model/items_model.py +821 -0
- kash/model/language_list.py +39 -0
- kash/model/llm_actions_model.py +63 -0
- kash/model/media_model.py +124 -0
- kash/model/operations_model.py +176 -0
- kash/model/params_model.py +435 -0
- kash/model/paths_model.py +458 -0
- kash/model/preconditions_model.py +98 -0
- kash/shell/__init__.py +0 -0
- kash/shell/completions/completion_scoring.py +280 -0
- kash/shell/completions/completion_types.py +154 -0
- kash/shell/completions/shell_completions.py +277 -0
- kash/shell/file_icons/color_for_format.py +70 -0
- kash/shell/file_icons/nerd_icons.py +946 -0
- kash/shell/output/__init__.py +0 -0
- kash/shell/output/kerm_code_utils.py +59 -0
- kash/shell/output/kerm_codes.py +588 -0
- kash/shell/output/kmarkdown.py +117 -0
- kash/shell/output/shell_output.py +477 -0
- kash/shell/ui/__init__.py +0 -0
- kash/shell/ui/shell_results.py +118 -0
- kash/shell/ui/shell_syntax.py +26 -0
- kash/shell/utils/exception_printing.py +50 -0
- kash/shell/utils/native_utils.py +240 -0
- kash/shell/utils/osc_utils.py +95 -0
- kash/shell/utils/shell_function_wrapper.py +204 -0
- kash/shell/utils/sys_tool_deps.py +289 -0
- kash/shell/utils/terminal_images.py +133 -0
- kash/shell_main.py +67 -0
- kash/text_handling/custom_sliding_transforms.py +266 -0
- kash/text_handling/doc_normalization.py +64 -0
- kash/text_handling/markdown_util.py +167 -0
- kash/text_handling/unified_diffs.py +138 -0
- kash/utils/__init__.py +4 -0
- kash/utils/common/__init__.py +4 -0
- kash/utils/common/atomic_var.py +147 -0
- kash/utils/common/format_utils.py +81 -0
- kash/utils/common/function_inspect.py +178 -0
- kash/utils/common/import_utils.py +89 -0
- kash/utils/common/lazyobject.py +144 -0
- kash/utils/common/obj_replace.py +78 -0
- kash/utils/common/parse_key_vals.py +85 -0
- kash/utils/common/parse_shell_args.py +348 -0
- kash/utils/common/stack_traces.py +49 -0
- kash/utils/common/string_replace.py +93 -0
- kash/utils/common/string_template.py +101 -0
- kash/utils/common/task_stack.py +162 -0
- kash/utils/common/type_utils.py +137 -0
- kash/utils/common/uniquifier.py +95 -0
- kash/utils/common/url.py +155 -0
- kash/utils/file_utils/__init__.py +3 -0
- kash/utils/file_utils/dir_size.py +48 -0
- kash/utils/file_utils/file_ext.py +86 -0
- kash/utils/file_utils/file_formats.py +134 -0
- kash/utils/file_utils/file_formats_model.py +408 -0
- kash/utils/file_utils/file_sort_filter.py +235 -0
- kash/utils/file_utils/file_walk.py +163 -0
- kash/utils/file_utils/filename_parsing.py +99 -0
- kash/utils/file_utils/git_tools.py +19 -0
- kash/utils/file_utils/ignore_files.py +166 -0
- kash/utils/file_utils/path_utils.py +36 -0
- kash/utils/lang_utils/__init__.py +0 -0
- kash/utils/lang_utils/capitalization.py +128 -0
- kash/utils/lang_utils/inflection.py +18 -0
- kash/utils/rich_custom/__init__.py +3 -0
- kash/utils/rich_custom/ansi_cell_len.py +72 -0
- kash/utils/rich_custom/rich_char_transform.py +89 -0
- kash/utils/rich_custom/rich_indent.py +69 -0
- kash/utils/rich_custom/rich_markdown_fork.py +771 -0
- kash/version.py +31 -0
- kash/web_content/canon_url.py +24 -0
- kash/web_content/dir_store.py +103 -0
- kash/web_content/file_cache_utils.py +117 -0
- kash/web_content/local_file_cache.py +247 -0
- kash/web_content/web_extract.py +55 -0
- kash/web_content/web_extract_justext.py +86 -0
- kash/web_content/web_extract_readabilipy.py +23 -0
- kash/web_content/web_fetch.py +101 -0
- kash/web_content/web_page_model.py +28 -0
- kash/web_gen/__init__.py +4 -0
- kash/web_gen/tabbed_webpage.py +149 -0
- kash/web_gen/template_render.py +29 -0
- kash/web_gen/templates/base_styles.css.jinja +192 -0
- kash/web_gen/templates/base_webpage.html.jinja +124 -0
- kash/web_gen/templates/content_styles.css.jinja +194 -0
- kash/web_gen/templates/explain_view.html.jinja +49 -0
- kash/web_gen/templates/item_view.html.jinja +294 -0
- kash/web_gen/templates/tabbed_webpage.html.jinja +49 -0
- kash/workspaces/__init__.py +13 -0
- kash/workspaces/param_state.py +24 -0
- kash/workspaces/selections.py +333 -0
- kash/workspaces/source_items.py +88 -0
- kash/workspaces/workspace_importing.py +56 -0
- kash/workspaces/workspace_names.py +33 -0
- kash/workspaces/workspace_output.py +154 -0
- kash/workspaces/workspace_registry.py +78 -0
- kash/workspaces/workspaces.py +197 -0
- kash/xonsh_custom/custom_shell.py +366 -0
- kash/xonsh_custom/customize_prompt.py +197 -0
- kash/xonsh_custom/customize_xonsh.py +112 -0
- kash/xonsh_custom/shell_load_commands.py +152 -0
- kash/xonsh_custom/shell_which.py +64 -0
- kash/xonsh_custom/xonsh_completers.py +715 -0
- kash/xonsh_custom/xonsh_env.py +28 -0
- kash/xonsh_custom/xonsh_modern_tools.py +56 -0
- kash/xonsh_custom/xonsh_ranking_completer.py +152 -0
- kash/xontrib/fnm.py +120 -0
- kash/xontrib/kash_extension.py +61 -0
- kash_shell-0.3.0.dist-info/METADATA +757 -0
- kash_shell-0.3.0.dist-info/RECORD +269 -0
- kash_shell-0.3.0.dist-info/WHEEL +4 -0
- kash_shell-0.3.0.dist-info/entry_points.txt +3 -0
- kash_shell-0.3.0.dist-info/licenses/LICENSE +664 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from kash.commands.base.show_command import show
|
|
4
|
+
from kash.commands.workspace.selection_commands import select
|
|
5
|
+
from kash.config.logger import get_logger
|
|
6
|
+
from kash.errors import InvalidInput, InvalidOperation
|
|
7
|
+
from kash.exec import import_locator_args, kash_command
|
|
8
|
+
from kash.exec_model.shell_model import ShellResult
|
|
9
|
+
from kash.model.items_model import Item, ItemType
|
|
10
|
+
from kash.shell.output.shell_output import Wrap, cprint
|
|
11
|
+
from kash.text_handling.unified_diffs import unified_diff_files, unified_diff_items
|
|
12
|
+
from kash.utils.file_utils.file_formats_model import Format
|
|
13
|
+
from kash.workspaces import current_ws
|
|
14
|
+
|
|
15
|
+
log = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@kash_command
|
|
19
|
+
def diff_items(*paths: str, force: bool = False) -> ShellResult:
|
|
20
|
+
"""
|
|
21
|
+
Show the unified diff between the given files. It's often helpful to treat diffs
|
|
22
|
+
as items themselves, so this works on items. Items are imported as usual into the
|
|
23
|
+
global workspace if they are not already in the store.
|
|
24
|
+
|
|
25
|
+
:param stat: Only show the diffstat summary.
|
|
26
|
+
:param force: If true, will run the diff even if the items are of different formats.
|
|
27
|
+
"""
|
|
28
|
+
ws = current_ws()
|
|
29
|
+
if len(paths) == 2:
|
|
30
|
+
[path1, path2] = paths
|
|
31
|
+
elif len(paths) == 0:
|
|
32
|
+
try:
|
|
33
|
+
last_selections = ws.selections.previous_n(2, expected_size=1)
|
|
34
|
+
except InvalidOperation:
|
|
35
|
+
raise InvalidInput(
|
|
36
|
+
"Need two selections of single files in history or exactly two paths to diff"
|
|
37
|
+
)
|
|
38
|
+
[path1] = last_selections[0].paths
|
|
39
|
+
[path2] = last_selections[1].paths
|
|
40
|
+
else:
|
|
41
|
+
raise InvalidInput("Provide zero paths (to use selections) or two paths to diff")
|
|
42
|
+
|
|
43
|
+
[store_path1, store_path2] = import_locator_args(path1, path2)
|
|
44
|
+
item1, item2 = ws.load(store_path1), ws.load(store_path2)
|
|
45
|
+
|
|
46
|
+
diff_item = unified_diff_items(item1, item2, strict=not force)
|
|
47
|
+
diff_store_path = ws.save(diff_item, as_tmp=False)
|
|
48
|
+
select(diff_store_path)
|
|
49
|
+
return ShellResult(show_selection=True)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@kash_command
|
|
53
|
+
def diff_files(*paths: str, diffstat: bool = False, save: bool = False) -> ShellResult:
|
|
54
|
+
"""
|
|
55
|
+
Show the unified diff between the given files. This works on any files, not
|
|
56
|
+
just items, so helpful for quick analysis without importing the files.
|
|
57
|
+
|
|
58
|
+
:param diffstat: Only show the diffstat summary.
|
|
59
|
+
:param save: Save the diff as an item in the store.
|
|
60
|
+
"""
|
|
61
|
+
if len(paths) == 2:
|
|
62
|
+
[path1, path2] = paths
|
|
63
|
+
elif len(paths) == 0:
|
|
64
|
+
# If nothing args given, user probably wants diff_items on selections.
|
|
65
|
+
return diff_items()
|
|
66
|
+
else:
|
|
67
|
+
raise InvalidInput("Provide zero paths (to use selections) or two paths to diff")
|
|
68
|
+
|
|
69
|
+
path1, path2 = Path(path1), Path(path2)
|
|
70
|
+
diff = unified_diff_files(path1, path2)
|
|
71
|
+
|
|
72
|
+
if diffstat:
|
|
73
|
+
cprint(diff.diffstat, text_wrap=Wrap.NONE)
|
|
74
|
+
return ShellResult(show_selection=False)
|
|
75
|
+
else:
|
|
76
|
+
diff_item = Item(
|
|
77
|
+
type=ItemType.doc,
|
|
78
|
+
title=f"Diff of {path1.name} and {path2.name}",
|
|
79
|
+
format=Format.diff,
|
|
80
|
+
body=diff.patch_text,
|
|
81
|
+
)
|
|
82
|
+
ws = current_ws()
|
|
83
|
+
if save:
|
|
84
|
+
diff_store_path = ws.save(diff_item, as_tmp=False)
|
|
85
|
+
select(diff_store_path)
|
|
86
|
+
return ShellResult(show_selection=True)
|
|
87
|
+
else:
|
|
88
|
+
diff_store_path = ws.save(diff_item, as_tmp=True)
|
|
89
|
+
show(diff_store_path)
|
|
90
|
+
return ShellResult(show_selection=False)
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
from datetime import UTC, datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
from prettyfmt import fmt_path, fmt_size_human, fmt_time
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
|
|
8
|
+
from kash.commands.workspace.selection_commands import select
|
|
9
|
+
from kash.config.logger import get_logger
|
|
10
|
+
from kash.config.settings import global_settings
|
|
11
|
+
from kash.config.text_styles import COLOR_EXTRA, EMOJI_WARN, STYLE_EMPH, STYLE_HINT, colorize_qty
|
|
12
|
+
from kash.exec import kash_command
|
|
13
|
+
from kash.exec_model.shell_model import ShellResult
|
|
14
|
+
from kash.local_server.local_url_formatters import local_url_formatter
|
|
15
|
+
from kash.model.items_model import Item, ItemType
|
|
16
|
+
from kash.model.paths_model import StorePath, parse_path_spec
|
|
17
|
+
from kash.shell.file_icons.color_for_format import color_for_format
|
|
18
|
+
from kash.shell.output.shell_output import PrintHooks, Wrap, console_pager, cprint
|
|
19
|
+
from kash.utils.file_utils.file_formats_model import Format, guess_format_by_name
|
|
20
|
+
from kash.utils.file_utils.file_sort_filter import (
|
|
21
|
+
FileInfo,
|
|
22
|
+
FileListing,
|
|
23
|
+
FileType,
|
|
24
|
+
GroupByOption,
|
|
25
|
+
SortOption,
|
|
26
|
+
collect_files,
|
|
27
|
+
parse_since,
|
|
28
|
+
type_suffix,
|
|
29
|
+
)
|
|
30
|
+
from kash.utils.file_utils.ignore_files import ignore_none
|
|
31
|
+
from kash.utils.file_utils.path_utils import common_parent_dir
|
|
32
|
+
from kash.workspaces import current_ignore, current_ws
|
|
33
|
+
|
|
34
|
+
log = get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _print_listing_tallies(
|
|
38
|
+
file_listing: FileListing,
|
|
39
|
+
total_displayed: int,
|
|
40
|
+
total_displayed_size: int,
|
|
41
|
+
max_files: int,
|
|
42
|
+
max_depth: int,
|
|
43
|
+
max_per_subdir: int,
|
|
44
|
+
) -> None:
|
|
45
|
+
if total_displayed > 0:
|
|
46
|
+
cprint(
|
|
47
|
+
f"{total_displayed} files ({fmt_size_human(total_displayed_size)}) shown",
|
|
48
|
+
style=COLOR_EXTRA,
|
|
49
|
+
)
|
|
50
|
+
if file_listing.files_total > file_listing.files_matching > total_displayed:
|
|
51
|
+
cprint(
|
|
52
|
+
f"of {file_listing.files_matching} files "
|
|
53
|
+
f"({fmt_size_human(file_listing.size_matching)}) matching criteria",
|
|
54
|
+
style=COLOR_EXTRA,
|
|
55
|
+
)
|
|
56
|
+
if file_listing.files_total > total_displayed:
|
|
57
|
+
cprint(
|
|
58
|
+
f"from {file_listing.files_total} total files "
|
|
59
|
+
f"({fmt_size_human(file_listing.size_total)})",
|
|
60
|
+
style=COLOR_EXTRA,
|
|
61
|
+
)
|
|
62
|
+
if file_listing.total_ignored > 0:
|
|
63
|
+
cprint(
|
|
64
|
+
f"{EMOJI_WARN} {file_listing.files_ignored} files and {file_listing.dirs_ignored} dirs were ignored",
|
|
65
|
+
style=COLOR_EXTRA,
|
|
66
|
+
)
|
|
67
|
+
cprint("(use --no_ignore to show hidden files)", style=STYLE_HINT)
|
|
68
|
+
|
|
69
|
+
if file_listing.total_skipped > 0:
|
|
70
|
+
cprint(
|
|
71
|
+
f"{EMOJI_WARN} long file listing: capped "
|
|
72
|
+
f"at max_files={max_files}, max_depth={max_depth}, max_per_subdir={max_per_subdir}",
|
|
73
|
+
style=COLOR_EXTRA,
|
|
74
|
+
)
|
|
75
|
+
cprint("(use --no_max to remove cutoff)", style=STYLE_HINT)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@kash_command
|
|
79
|
+
def files(
|
|
80
|
+
*paths: str,
|
|
81
|
+
overview: bool = False,
|
|
82
|
+
recent: bool = False,
|
|
83
|
+
recursive: bool = False,
|
|
84
|
+
flat: bool = False,
|
|
85
|
+
pager: bool = False,
|
|
86
|
+
omit_dirs: bool = False,
|
|
87
|
+
max_per_group: int = -1,
|
|
88
|
+
depth: int | None = None,
|
|
89
|
+
max_per_subdir: int = 1000,
|
|
90
|
+
max_files: int = 1000,
|
|
91
|
+
no_max: bool = False,
|
|
92
|
+
no_ignore: bool = False,
|
|
93
|
+
all: bool = False,
|
|
94
|
+
save: bool = False,
|
|
95
|
+
sort: SortOption | None = None,
|
|
96
|
+
reverse: bool = False,
|
|
97
|
+
since: str | None = None,
|
|
98
|
+
groupby: GroupByOption | None = GroupByOption.parent,
|
|
99
|
+
iso_time: bool = False,
|
|
100
|
+
) -> ShellResult:
|
|
101
|
+
"""
|
|
102
|
+
List files or folders in the current directory or specified paths.
|
|
103
|
+
|
|
104
|
+
Attempts to be similar to `ls` or `eza` but without any legacy constraints.
|
|
105
|
+
|
|
106
|
+
Aims for simple output, optional paging, a better "overview" mode that recurses
|
|
107
|
+
with limited depth and breadth, and more control over recursion, sorting,
|
|
108
|
+
and grouping.
|
|
109
|
+
|
|
110
|
+
:param overview: Recurse a couple levels and show files, but not too many.
|
|
111
|
+
Same as `--groupby=parent --depth=2 --max_per_group=10 --omit_dirs`.
|
|
112
|
+
:param recent: Only shows the most recently modified files in each directory.
|
|
113
|
+
Same as `--sort=modified --reverse --groupby=parent --max_per_group=10`.
|
|
114
|
+
:param recursive: List all files recursively. Same as `--depth=-1`.
|
|
115
|
+
:param flat: Show files in a flat list, rather than grouped by parent directory.
|
|
116
|
+
Same as `--groupby=flat`.
|
|
117
|
+
:param omit_dirs: Normally directories are included. This option omits them,
|
|
118
|
+
which is useful when recursing into subdirectories.
|
|
119
|
+
:param depth: Maximum depth to recurse into directories. -1 means no limit.
|
|
120
|
+
:param max_files: Maximum number of files to yield per input path.
|
|
121
|
+
-1 means no limit.
|
|
122
|
+
:param max_per_subdir: Maximum number of files to yield per subdirectory
|
|
123
|
+
(not including the top level). -1 means no limit.
|
|
124
|
+
:param max_per_group: Limit the first number of items displayed per group
|
|
125
|
+
(if groupby is used) or in total. 0 means show all.
|
|
126
|
+
:param no_max: Disable limits on depth and number of files. Same as
|
|
127
|
+
`--depth=-1 --max_files=-1 --max_per_subdir=-1 --max_per_group=-1`.
|
|
128
|
+
:param no_ignore: Disable ignoring hidden files.
|
|
129
|
+
:param all: Same as `--no_ignore --no_max`. Does not change `--depth`.
|
|
130
|
+
:param save: Save the listing as a CSV file item.
|
|
131
|
+
:param sort: Sort by `filename`, `size`, `accessed`, `created`, or `modified`.
|
|
132
|
+
:param reverse: Reverse the sorting order.
|
|
133
|
+
:param since: Filter files modified since a given time (e.g., '1 day', '2 hours').
|
|
134
|
+
:param groupby: Group results. Can be `flat` (no grouping, and by default implies
|
|
135
|
+
recursive), `parent`, or `suffix`. Defaults to 'parent'.
|
|
136
|
+
:param iso_time: Show time in ISO format (default is human-readable age).
|
|
137
|
+
:param pager: Use the pager when displaying the output.
|
|
138
|
+
"""
|
|
139
|
+
if global_settings().use_nerd_icons:
|
|
140
|
+
from kash.shell.file_icons.nerd_icons import icon_for_file
|
|
141
|
+
else:
|
|
142
|
+
icon_for_file = None
|
|
143
|
+
|
|
144
|
+
# TODO: Add a --full option with line and word counts and file_info details
|
|
145
|
+
# and also include these in --save.
|
|
146
|
+
|
|
147
|
+
if len(paths) == 0:
|
|
148
|
+
paths_to_show = [Path(".")]
|
|
149
|
+
no_explicit_paths = True
|
|
150
|
+
else:
|
|
151
|
+
paths_to_show = [parse_path_spec(path) for path in paths]
|
|
152
|
+
no_explicit_paths = False
|
|
153
|
+
|
|
154
|
+
# Set up base path. If we have a workspace and if this listing is within the
|
|
155
|
+
# current workspace, detect that, since it's convenient to enable brief listings
|
|
156
|
+
# in workspaces.
|
|
157
|
+
cwd = Path.cwd()
|
|
158
|
+
ws = current_ws()
|
|
159
|
+
active_ws_name = ws.name if cwd.is_relative_to(ws.base_dir.resolve()) else None
|
|
160
|
+
# Check if all requested paths are within the current directory, and if so use
|
|
161
|
+
# that as the base path. Otherwise, use the common parent directory of all paths.
|
|
162
|
+
if paths_to_show:
|
|
163
|
+
base_path = common_parent_dir(*paths_to_show)
|
|
164
|
+
if base_path.is_relative_to(cwd):
|
|
165
|
+
base_path = cwd
|
|
166
|
+
within_cwd = base_path.is_relative_to(cwd)
|
|
167
|
+
else:
|
|
168
|
+
base_path = Path(".")
|
|
169
|
+
within_cwd = True
|
|
170
|
+
# Should we show absolute paths?
|
|
171
|
+
show_absolute_paths = not within_cwd
|
|
172
|
+
# Is this a listing of the current workspace?
|
|
173
|
+
is_ws_listing = active_ws_name and ws.base_dir.resolve() == base_path.resolve()
|
|
174
|
+
|
|
175
|
+
# Handle lots of different options.
|
|
176
|
+
if recursive:
|
|
177
|
+
depth = -1
|
|
178
|
+
if is_ws_listing and no_explicit_paths:
|
|
179
|
+
# Within workspaces, we show more files by default since they are always in
|
|
180
|
+
# subdirectories.
|
|
181
|
+
overview = True # Handled next.
|
|
182
|
+
if overview:
|
|
183
|
+
max_per_group = 10 if max_per_group <= 0 else max_per_group
|
|
184
|
+
groupby = GroupByOption.parent if groupby is None else groupby
|
|
185
|
+
depth = 2 if depth is None else depth
|
|
186
|
+
omit_dirs = True
|
|
187
|
+
if recent:
|
|
188
|
+
max_per_group = 10 if max_per_group <= 0 else max_per_group
|
|
189
|
+
groupby = GroupByOption.parent if groupby is None else groupby
|
|
190
|
+
depth = 2 if depth is None else depth
|
|
191
|
+
sort = SortOption.modified if sort is None else sort
|
|
192
|
+
reverse = True
|
|
193
|
+
if flat:
|
|
194
|
+
groupby = GroupByOption.flat
|
|
195
|
+
if all:
|
|
196
|
+
no_ignore = True
|
|
197
|
+
no_max = True
|
|
198
|
+
if no_max:
|
|
199
|
+
depth = max_per_subdir = max_per_group = max_files = -1
|
|
200
|
+
# Unless depth is specified, flat implies recursive (depth -1).
|
|
201
|
+
if groupby == GroupByOption.flat and depth is None:
|
|
202
|
+
depth = -1
|
|
203
|
+
|
|
204
|
+
# Default depth unless otherwise set or implied is 0 (like ls).
|
|
205
|
+
depth = 0 if depth is None else depth
|
|
206
|
+
|
|
207
|
+
since_seconds = parse_since(since) if since else 0.0
|
|
208
|
+
|
|
209
|
+
# Determine whether to show hidden files for this path.
|
|
210
|
+
is_ignored = current_ignore()
|
|
211
|
+
if no_ignore:
|
|
212
|
+
is_ignored = ignore_none
|
|
213
|
+
else:
|
|
214
|
+
for path in paths_to_show:
|
|
215
|
+
# log.info("Checking ignore for %s against filter %s", fmt_path(path), is_ignored)
|
|
216
|
+
if not no_ignore and is_ignored(path, is_dir=path.is_dir()):
|
|
217
|
+
log.info(
|
|
218
|
+
"Requested path is on the ignore list so disabling ignore: %s",
|
|
219
|
+
fmt_path(path),
|
|
220
|
+
)
|
|
221
|
+
is_ignored = ignore_none
|
|
222
|
+
break
|
|
223
|
+
|
|
224
|
+
# Collect all the files.
|
|
225
|
+
log.debug(
|
|
226
|
+
"Collecting files: %s",
|
|
227
|
+
{
|
|
228
|
+
"paths_to_show": paths_to_show,
|
|
229
|
+
"depth": depth,
|
|
230
|
+
"max_per_subdir": max_per_subdir,
|
|
231
|
+
"max_per_group": max_per_group,
|
|
232
|
+
"max_files": max_files,
|
|
233
|
+
"omit_dirs": omit_dirs,
|
|
234
|
+
"since_seconds": since_seconds,
|
|
235
|
+
"base_path": base_path,
|
|
236
|
+
"include_dirs": not omit_dirs,
|
|
237
|
+
},
|
|
238
|
+
)
|
|
239
|
+
file_listing = collect_files(
|
|
240
|
+
start_paths=paths_to_show,
|
|
241
|
+
ignore=is_ignored,
|
|
242
|
+
since_seconds=since_seconds,
|
|
243
|
+
max_depth=depth,
|
|
244
|
+
max_files_per_subdir=max_per_subdir,
|
|
245
|
+
max_files_total=max_files,
|
|
246
|
+
base_path=base_path,
|
|
247
|
+
include_dirs=not omit_dirs,
|
|
248
|
+
resolve_parent=show_absolute_paths,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
log.info("Collected %s files.", file_listing.files_total)
|
|
252
|
+
|
|
253
|
+
if not file_listing.files:
|
|
254
|
+
cprint("No files found.")
|
|
255
|
+
PrintHooks.spacer()
|
|
256
|
+
_print_listing_tallies(file_listing, 0, 0, max_files, depth, max_per_subdir)
|
|
257
|
+
return ShellResult()
|
|
258
|
+
|
|
259
|
+
df = file_listing.as_dataframe()
|
|
260
|
+
|
|
261
|
+
if sort:
|
|
262
|
+
# Determine the primary and secondary sort columns.
|
|
263
|
+
primary_sort = sort.value
|
|
264
|
+
secondary_sort = "filename" if primary_sort != "filename" else "created"
|
|
265
|
+
|
|
266
|
+
df.sort_values(
|
|
267
|
+
by=[primary_sort, secondary_sort], ascending=[not reverse, True], inplace=True
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
items_matching = len(df)
|
|
271
|
+
log.info(f"Total items collected: {items_matching}")
|
|
272
|
+
|
|
273
|
+
if groupby and groupby != GroupByOption.flat:
|
|
274
|
+
grouped = df.groupby(groupby.value)
|
|
275
|
+
else:
|
|
276
|
+
grouped = [(None, df)]
|
|
277
|
+
|
|
278
|
+
if save:
|
|
279
|
+
item = Item(
|
|
280
|
+
type=ItemType.export,
|
|
281
|
+
title="File Listing",
|
|
282
|
+
description=f"Files in {', '.join(fmt_path(p) for p in paths_to_show)}",
|
|
283
|
+
format=Format.csv,
|
|
284
|
+
body=df.to_csv(index=False),
|
|
285
|
+
)
|
|
286
|
+
ws = current_ws()
|
|
287
|
+
store_path = ws.save(item, as_tmp=False)
|
|
288
|
+
log.message("File listing saved to: %s", fmt_path(store_path))
|
|
289
|
+
|
|
290
|
+
select(store_path)
|
|
291
|
+
|
|
292
|
+
return ShellResult(show_selection=True)
|
|
293
|
+
|
|
294
|
+
total_displayed = 0
|
|
295
|
+
total_displayed_size = 0
|
|
296
|
+
now = datetime.now(UTC)
|
|
297
|
+
|
|
298
|
+
# Define spacing constants.
|
|
299
|
+
TIME_WIDTH = 12
|
|
300
|
+
SIZE_WIDTH = 8
|
|
301
|
+
SPACING = " "
|
|
302
|
+
indent = " " * (TIME_WIDTH + SIZE_WIDTH + len(SPACING) * 2)
|
|
303
|
+
|
|
304
|
+
with console_pager(use_pager=pager):
|
|
305
|
+
with local_url_formatter(active_ws_name) as fmt:
|
|
306
|
+
for group_name, group_df in grouped:
|
|
307
|
+
# If items are grouped e.g. by parent directory, show the group name first.
|
|
308
|
+
if group_name:
|
|
309
|
+
cprint(
|
|
310
|
+
f"{group_name} ({len(group_df)} files)",
|
|
311
|
+
style=STYLE_EMPH,
|
|
312
|
+
text_wrap=Wrap.NONE,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
if max_per_group > 0:
|
|
316
|
+
display_df = group_df.head(max_per_group)
|
|
317
|
+
else:
|
|
318
|
+
display_df = group_df
|
|
319
|
+
|
|
320
|
+
for row in display_df.itertuples(index=False, name="FileInfo"):
|
|
321
|
+
row = cast(FileInfo, row) # pyright: ignore
|
|
322
|
+
short_file_size = fmt_size_human(row.size)
|
|
323
|
+
full_file_size = f"{row.size} bytes"
|
|
324
|
+
short_mod_time = fmt_time(row.modified, iso_time=iso_time, now=now, brief=True)
|
|
325
|
+
full_mod_time = fmt_time(row.modified, friendly=True, now=now)
|
|
326
|
+
is_dir = row.type == FileType.dir
|
|
327
|
+
|
|
328
|
+
rel_path = str(row.relative_path)
|
|
329
|
+
|
|
330
|
+
# If we are listing from within a workspace and we are at the base
|
|
331
|
+
# of the workspace, we include the paths as store paths (with an @
|
|
332
|
+
# prefix). Otherwise, use regular paths.
|
|
333
|
+
if is_ws_listing:
|
|
334
|
+
display_path = StorePath(rel_path) # Add a local server link.
|
|
335
|
+
display_path_str = f"{display_path}{type_suffix(row)}"
|
|
336
|
+
else:
|
|
337
|
+
display_path = Path(rel_path)
|
|
338
|
+
display_path_str = f"{display_path}{type_suffix(row)}"
|
|
339
|
+
|
|
340
|
+
# Assemble output line.
|
|
341
|
+
line: list[str | Text] = []
|
|
342
|
+
line.append(
|
|
343
|
+
colorize_qty(
|
|
344
|
+
fmt.tooltip_link(
|
|
345
|
+
short_mod_time.rjust(TIME_WIDTH), tooltip=full_mod_time
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
line.append(SPACING)
|
|
350
|
+
if is_dir:
|
|
351
|
+
# TODO: Insert tallies of files/total size in a fast/efficient way.
|
|
352
|
+
line.append(" " * SIZE_WIDTH)
|
|
353
|
+
else:
|
|
354
|
+
line.append(
|
|
355
|
+
colorize_qty(
|
|
356
|
+
fmt.tooltip_link(
|
|
357
|
+
short_file_size.rjust(SIZE_WIDTH), tooltip=full_file_size
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
line.append(SPACING)
|
|
362
|
+
if icon_for_file:
|
|
363
|
+
icon = icon_for_file(rel_path, is_dir=is_dir)
|
|
364
|
+
color = color_for_format(guess_format_by_name(rel_path))
|
|
365
|
+
line.append(
|
|
366
|
+
fmt.tooltip_link(icon.icon_char, tooltip=icon.readable, style=color)
|
|
367
|
+
)
|
|
368
|
+
line.append(" ")
|
|
369
|
+
line.append(
|
|
370
|
+
fmt.path_link(
|
|
371
|
+
display_path,
|
|
372
|
+
link_text=display_path_str,
|
|
373
|
+
),
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
cprint(Text.assemble(*line), text_wrap=Wrap.NONE)
|
|
377
|
+
total_displayed += 1
|
|
378
|
+
total_displayed_size += row.size
|
|
379
|
+
|
|
380
|
+
# Indicate if items are omitted.
|
|
381
|
+
if groupby and max_per_group > 0 and len(group_df) > max_per_group:
|
|
382
|
+
cprint(
|
|
383
|
+
f"{indent}… and {len(group_df) - max_per_group} more files",
|
|
384
|
+
style=COLOR_EXTRA,
|
|
385
|
+
text_wrap=Wrap.NONE,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
if group_name:
|
|
389
|
+
PrintHooks.spacer()
|
|
390
|
+
|
|
391
|
+
if not groupby and max_per_group > 0 and items_matching > max_per_group:
|
|
392
|
+
cprint(
|
|
393
|
+
f"{indent}… and {items_matching - max_per_group} more files",
|
|
394
|
+
style=COLOR_EXTRA,
|
|
395
|
+
text_wrap=Wrap.NONE,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
PrintHooks.spacer()
|
|
399
|
+
_print_listing_tallies(
|
|
400
|
+
file_listing,
|
|
401
|
+
total_displayed,
|
|
402
|
+
total_displayed_size,
|
|
403
|
+
max_files,
|
|
404
|
+
depth,
|
|
405
|
+
max_per_subdir,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
return ShellResult()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from kash.config.api_keys import print_api_key_setup, warn_if_missing_api_keys
|
|
2
|
+
from kash.config.logger import get_logger
|
|
3
|
+
from kash.docs.all_docs import all_docs
|
|
4
|
+
from kash.exec import kash_command
|
|
5
|
+
from kash.help.tldr_help import tldr_refresh_cache
|
|
6
|
+
from kash.shell.output.shell_output import cprint, format_name_and_value
|
|
7
|
+
from kash.shell.utils.sys_tool_deps import sys_tool_check, terminal_feature_check
|
|
8
|
+
|
|
9
|
+
log = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@kash_command
|
|
13
|
+
def version() -> None:
|
|
14
|
+
"""
|
|
15
|
+
Show the version of kash.
|
|
16
|
+
"""
|
|
17
|
+
from kash.shell_main import APP_VERSION
|
|
18
|
+
|
|
19
|
+
cprint(APP_VERSION)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@kash_command
|
|
23
|
+
def self_check(brief: bool = False) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Self-check kash setup, including termal settings, tools, and API keys.
|
|
26
|
+
"""
|
|
27
|
+
if brief:
|
|
28
|
+
terminal_feature_check().print_term_info()
|
|
29
|
+
print_api_key_setup(once=False)
|
|
30
|
+
check_tools(brief=brief)
|
|
31
|
+
tldr_refresh_cache()
|
|
32
|
+
try:
|
|
33
|
+
all_docs.load()
|
|
34
|
+
except Exception as e:
|
|
35
|
+
log.error("Could not index docs: %s", e)
|
|
36
|
+
raise e
|
|
37
|
+
else:
|
|
38
|
+
version()
|
|
39
|
+
cprint()
|
|
40
|
+
terminal_feature_check().print_term_info()
|
|
41
|
+
cprint()
|
|
42
|
+
print_api_key_setup(once=False)
|
|
43
|
+
cprint()
|
|
44
|
+
check_tools(brief=brief)
|
|
45
|
+
cprint()
|
|
46
|
+
if tldr_refresh_cache():
|
|
47
|
+
cprint("Updated tldr cache")
|
|
48
|
+
else:
|
|
49
|
+
cprint("tldr cache is up to date")
|
|
50
|
+
try:
|
|
51
|
+
all_docs.load()
|
|
52
|
+
except Exception as e:
|
|
53
|
+
log.error("Could not index docs: %s", e)
|
|
54
|
+
raise e
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@kash_command
|
|
58
|
+
def check_tools(warn_only: bool = False, brief: bool = False) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Check that all tools are installed.
|
|
61
|
+
|
|
62
|
+
:param warn_only: Only warn if tools are missing.
|
|
63
|
+
:param brief: Print summary as a single line.
|
|
64
|
+
"""
|
|
65
|
+
if warn_only:
|
|
66
|
+
sys_tool_check().warn_if_missing()
|
|
67
|
+
else:
|
|
68
|
+
if brief:
|
|
69
|
+
cprint(sys_tool_check().status())
|
|
70
|
+
else:
|
|
71
|
+
cprint("Checking for required tools:")
|
|
72
|
+
cprint()
|
|
73
|
+
cprint(sys_tool_check().formatted())
|
|
74
|
+
cprint()
|
|
75
|
+
sys_tool_check().warn_if_missing()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@kash_command
|
|
79
|
+
def check_api_keys(warn_only: bool = False) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Check that all recommended API keys are set.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
if warn_only:
|
|
85
|
+
warn_if_missing_api_keys()
|
|
86
|
+
else:
|
|
87
|
+
print_api_key_setup()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@kash_command
|
|
91
|
+
def kits() -> None:
|
|
92
|
+
"""
|
|
93
|
+
List all kits (modules within `kash.kits`).
|
|
94
|
+
"""
|
|
95
|
+
from kash.actions import get_loaded_kits
|
|
96
|
+
|
|
97
|
+
if not get_loaded_kits():
|
|
98
|
+
cprint(
|
|
99
|
+
"No kits currently imported (be sure the Python environment has `kash.kits` modules in the load path)"
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
cprint("Currently imported kits:")
|
|
103
|
+
for kit in get_loaded_kits().values():
|
|
104
|
+
cprint(format_name_and_value(f"{kit.name} kit", str(kit.path or "")))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from kash.commands.base.basic_file_commands import trash
|
|
2
|
+
from kash.config.logger import get_logger
|
|
3
|
+
from kash.exec import kash_command
|
|
4
|
+
from kash.shell.output.shell_output import PrintHooks, cprint, format_name_and_value, print_h2
|
|
5
|
+
|
|
6
|
+
log = get_logger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@kash_command
|
|
10
|
+
def global_settings() -> None:
|
|
11
|
+
"""
|
|
12
|
+
Show all global kash settings.
|
|
13
|
+
"""
|
|
14
|
+
from kash.config.settings import global_settings
|
|
15
|
+
|
|
16
|
+
settings = global_settings()
|
|
17
|
+
print_h2("Global Settings")
|
|
18
|
+
for field, value in settings.__dict__.items():
|
|
19
|
+
cprint(format_name_and_value(field, str(value)))
|
|
20
|
+
PrintHooks.spacer()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@kash_command
|
|
24
|
+
def clear_global_cache(media: bool = False, content: bool = False) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Clear the global media and content caches. By default clears both caches.
|
|
27
|
+
|
|
28
|
+
:param media: Clear the media cache only.
|
|
29
|
+
:param content: Clear the content cache only.
|
|
30
|
+
"""
|
|
31
|
+
from kash.config.settings import global_settings
|
|
32
|
+
|
|
33
|
+
if not media and not content:
|
|
34
|
+
media = True
|
|
35
|
+
content = True
|
|
36
|
+
|
|
37
|
+
if media and global_settings().media_cache_dir.exists():
|
|
38
|
+
trash(global_settings().media_cache_dir)
|
|
39
|
+
|
|
40
|
+
if content and global_settings().content_cache_dir.exists():
|
|
41
|
+
trash(global_settings().content_cache_dir)
|