Compare commits
3 Commits
446f52ca6d
...
6c765ff7b5
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c765ff7b5 | |||
| 6682040cc0 | |||
| ab1c561d6c |
+67
-1
@@ -1,3 +1,69 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
// App-ACL-Manifest: erzeugt pro Command allow-/deny-Permissions. Sobald das
|
||||
// Manifest existiert, gilt deny-by-default — jedes Command muss in einer
|
||||
// Capability einem Fenster zugeteilt sein (capabilities/*.json), sonst
|
||||
// lehnt Tauri den Aufruf ab. Neue Commands hier UND in der passenden
|
||||
// Capability eintragen.
|
||||
tauri_build::try_build(
|
||||
tauri_build::Attributes::new().app_manifest(tauri_build::AppManifest::new().commands(&[
|
||||
"list_projects",
|
||||
"create_project_full",
|
||||
"add_project",
|
||||
"delete_preview",
|
||||
"delete_project_scoped",
|
||||
"project_work_dirs",
|
||||
"set_project_dir",
|
||||
"add_work_dir",
|
||||
"remove_work_dir",
|
||||
"list_pools",
|
||||
"create_oauth_pool",
|
||||
"create_apikey_pool",
|
||||
"rename_pool",
|
||||
"delete_pool",
|
||||
"assign_pool",
|
||||
"unassign_pool",
|
||||
"set_terminal_config",
|
||||
"project_icon",
|
||||
"pool_label",
|
||||
"todo_state",
|
||||
"set_todo",
|
||||
"usage_stats",
|
||||
"stop_project",
|
||||
"restart_project",
|
||||
"start_or_focus_cmd",
|
||||
"open_main_window",
|
||||
"quit_app",
|
||||
"sync_setting",
|
||||
"set_sync_setting",
|
||||
"terminal_font_size",
|
||||
"set_terminal_font_size",
|
||||
"link_pool_runtime",
|
||||
"oauth_login",
|
||||
"keychain_status",
|
||||
"set_apikey",
|
||||
"panel_archive_dir_cmd",
|
||||
"panel_archive_cmd",
|
||||
"set_archive_home_cmd",
|
||||
"clear_archive_home_cmd",
|
||||
"archive_docs_cmd",
|
||||
"reveal_path_cmd",
|
||||
"spellcheck_lang",
|
||||
"open_terminal",
|
||||
"term_start",
|
||||
"term_log",
|
||||
"term_write",
|
||||
"term_resize",
|
||||
"panel_read",
|
||||
"commands_read",
|
||||
"commands_delete",
|
||||
"panel_set",
|
||||
"search_read",
|
||||
"search_run",
|
||||
"panel_load",
|
||||
"wiki_read",
|
||||
"wiki_open",
|
||||
"open_panel_window",
|
||||
])),
|
||||
)
|
||||
.expect("tauri-build");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "main",
|
||||
"description": "Hauptfenster: komplette Projekt-/Pool-Verwaltung.",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-set-size",
|
||||
"core:window:allow-set-position",
|
||||
"core:window:allow-outer-position",
|
||||
"core:window:allow-inner-size",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-start-resize-dragging",
|
||||
"autostart:default",
|
||||
"dialog:default",
|
||||
"clipboard-manager:allow-write-text",
|
||||
"allow-list-projects",
|
||||
"allow-create-project-full",
|
||||
"allow-add-project",
|
||||
"allow-project-work-dirs",
|
||||
"allow-set-project-dir",
|
||||
"allow-add-work-dir",
|
||||
"allow-remove-work-dir",
|
||||
"allow-list-pools",
|
||||
"allow-create-oauth-pool",
|
||||
"allow-create-apikey-pool",
|
||||
"allow-rename-pool",
|
||||
"allow-delete-pool",
|
||||
"allow-assign-pool",
|
||||
"allow-unassign-pool",
|
||||
"allow-set-terminal-config",
|
||||
"allow-project-icon",
|
||||
"allow-todo-state",
|
||||
"allow-set-todo",
|
||||
"allow-usage-stats",
|
||||
"allow-stop-project",
|
||||
"allow-restart-project",
|
||||
"allow-open-terminal",
|
||||
"allow-sync-setting",
|
||||
"allow-set-sync-setting",
|
||||
"allow-link-pool-runtime",
|
||||
"allow-oauth-login",
|
||||
"allow-keychain-status",
|
||||
"allow-set-apikey",
|
||||
"allow-panel-archive-dir-cmd",
|
||||
"allow-set-archive-home-cmd",
|
||||
"allow-clear-archive-home-cmd",
|
||||
"allow-archive-docs-cmd",
|
||||
"allow-delete-preview",
|
||||
"allow-delete-project-scoped"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "panel",
|
||||
"description": "Abgelöstes Panel-Fenster: nur Panel-/Archiv-Kanäle, keine PTY, keine Verwaltung.",
|
||||
"windows": [
|
||||
"panel-*"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-set-size",
|
||||
"core:window:allow-set-position",
|
||||
"core:window:allow-outer-position",
|
||||
"core:window:allow-inner-size",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-start-resize-dragging",
|
||||
"autostart:default",
|
||||
"dialog:default",
|
||||
"clipboard-manager:allow-write-text",
|
||||
"allow-commands-delete",
|
||||
"allow-commands-read",
|
||||
"allow-list-projects",
|
||||
"allow-panel-archive-cmd",
|
||||
"allow-panel-archive-dir-cmd",
|
||||
"allow-panel-load",
|
||||
"allow-panel-read",
|
||||
"allow-panel-set",
|
||||
"allow-reveal-path-cmd",
|
||||
"allow-search-read",
|
||||
"allow-search-run",
|
||||
"allow-spellcheck-lang",
|
||||
"allow-term-log",
|
||||
"allow-wiki-open",
|
||||
"allow-wiki-read"
|
||||
]
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"identifier": "popup",
|
||||
"description": "Tray-Popup: Projektliste anzeigen und starten.",
|
||||
"windows": [
|
||||
"main",
|
||||
"popup",
|
||||
"term-*",
|
||||
"panel-*"
|
||||
"popup"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
@@ -22,6 +19,11 @@
|
||||
"core:window:allow-start-resize-dragging",
|
||||
"autostart:default",
|
||||
"dialog:default",
|
||||
"clipboard-manager:allow-write-text"
|
||||
"clipboard-manager:allow-write-text",
|
||||
"allow-list-projects",
|
||||
"allow-project-icon",
|
||||
"allow-start-or-focus-cmd",
|
||||
"allow-open-main-window",
|
||||
"allow-quit-app"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "terminal",
|
||||
"description": "Terminal-Fenster: PTY und angedocktes Panel.",
|
||||
"windows": [
|
||||
"term-*"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-set-size",
|
||||
"core:window:allow-set-position",
|
||||
"core:window:allow-outer-position",
|
||||
"core:window:allow-inner-size",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-start-resize-dragging",
|
||||
"autostart:default",
|
||||
"dialog:default",
|
||||
"clipboard-manager:allow-write-text",
|
||||
"allow-commands-delete",
|
||||
"allow-commands-read",
|
||||
"allow-list-projects",
|
||||
"allow-open-panel-window",
|
||||
"allow-panel-archive-cmd",
|
||||
"allow-panel-archive-dir-cmd",
|
||||
"allow-panel-load",
|
||||
"allow-panel-read",
|
||||
"allow-panel-set",
|
||||
"allow-pool-label",
|
||||
"allow-project-icon",
|
||||
"allow-reveal-path-cmd",
|
||||
"allow-search-read",
|
||||
"allow-search-run",
|
||||
"allow-set-terminal-font-size",
|
||||
"allow-spellcheck-lang",
|
||||
"allow-term-log",
|
||||
"allow-term-resize",
|
||||
"allow-term-start",
|
||||
"allow-term-write",
|
||||
"allow-terminal-font-size",
|
||||
"allow-wiki-open",
|
||||
"allow-wiki-read"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-add-project"
|
||||
description = "Enables the add_project command without any pre-configured scope."
|
||||
commands.allow = ["add_project"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-add-project"
|
||||
description = "Denies the add_project command without any pre-configured scope."
|
||||
commands.deny = ["add_project"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-add-work-dir"
|
||||
description = "Enables the add_work_dir command without any pre-configured scope."
|
||||
commands.allow = ["add_work_dir"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-add-work-dir"
|
||||
description = "Denies the add_work_dir command without any pre-configured scope."
|
||||
commands.deny = ["add_work_dir"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-archive-docs-cmd"
|
||||
description = "Enables the archive_docs_cmd command without any pre-configured scope."
|
||||
commands.allow = ["archive_docs_cmd"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-archive-docs-cmd"
|
||||
description = "Denies the archive_docs_cmd command without any pre-configured scope."
|
||||
commands.deny = ["archive_docs_cmd"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-assign-pool"
|
||||
description = "Enables the assign_pool command without any pre-configured scope."
|
||||
commands.allow = ["assign_pool"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-assign-pool"
|
||||
description = "Denies the assign_pool command without any pre-configured scope."
|
||||
commands.deny = ["assign_pool"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-clear-archive-home-cmd"
|
||||
description = "Enables the clear_archive_home_cmd command without any pre-configured scope."
|
||||
commands.allow = ["clear_archive_home_cmd"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-clear-archive-home-cmd"
|
||||
description = "Denies the clear_archive_home_cmd command without any pre-configured scope."
|
||||
commands.deny = ["clear_archive_home_cmd"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-commands-delete"
|
||||
description = "Enables the commands_delete command without any pre-configured scope."
|
||||
commands.allow = ["commands_delete"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-commands-delete"
|
||||
description = "Denies the commands_delete command without any pre-configured scope."
|
||||
commands.deny = ["commands_delete"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-commands-read"
|
||||
description = "Enables the commands_read command without any pre-configured scope."
|
||||
commands.allow = ["commands_read"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-commands-read"
|
||||
description = "Denies the commands_read command without any pre-configured scope."
|
||||
commands.deny = ["commands_read"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-create-apikey-pool"
|
||||
description = "Enables the create_apikey_pool command without any pre-configured scope."
|
||||
commands.allow = ["create_apikey_pool"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-create-apikey-pool"
|
||||
description = "Denies the create_apikey_pool command without any pre-configured scope."
|
||||
commands.deny = ["create_apikey_pool"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-create-oauth-pool"
|
||||
description = "Enables the create_oauth_pool command without any pre-configured scope."
|
||||
commands.allow = ["create_oauth_pool"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-create-oauth-pool"
|
||||
description = "Denies the create_oauth_pool command without any pre-configured scope."
|
||||
commands.deny = ["create_oauth_pool"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-create-project-full"
|
||||
description = "Enables the create_project_full command without any pre-configured scope."
|
||||
commands.allow = ["create_project_full"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-create-project-full"
|
||||
description = "Denies the create_project_full command without any pre-configured scope."
|
||||
commands.deny = ["create_project_full"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-delete-pool"
|
||||
description = "Enables the delete_pool command without any pre-configured scope."
|
||||
commands.allow = ["delete_pool"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-delete-pool"
|
||||
description = "Denies the delete_pool command without any pre-configured scope."
|
||||
commands.deny = ["delete_pool"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-delete-preview"
|
||||
description = "Enables the delete_preview command without any pre-configured scope."
|
||||
commands.allow = ["delete_preview"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-delete-preview"
|
||||
description = "Denies the delete_preview command without any pre-configured scope."
|
||||
commands.deny = ["delete_preview"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-delete-project"
|
||||
description = "Enables the delete_project command without any pre-configured scope."
|
||||
commands.allow = ["delete_project"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-delete-project"
|
||||
description = "Denies the delete_project command without any pre-configured scope."
|
||||
commands.deny = ["delete_project"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-delete-project-scoped"
|
||||
description = "Enables the delete_project_scoped command without any pre-configured scope."
|
||||
commands.allow = ["delete_project_scoped"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-delete-project-scoped"
|
||||
description = "Denies the delete_project_scoped command without any pre-configured scope."
|
||||
commands.deny = ["delete_project_scoped"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-keychain-status"
|
||||
description = "Enables the keychain_status command without any pre-configured scope."
|
||||
commands.allow = ["keychain_status"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-keychain-status"
|
||||
description = "Denies the keychain_status command without any pre-configured scope."
|
||||
commands.deny = ["keychain_status"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-link-pool-runtime"
|
||||
description = "Enables the link_pool_runtime command without any pre-configured scope."
|
||||
commands.allow = ["link_pool_runtime"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-link-pool-runtime"
|
||||
description = "Denies the link_pool_runtime command without any pre-configured scope."
|
||||
commands.deny = ["link_pool_runtime"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-list-pools"
|
||||
description = "Enables the list_pools command without any pre-configured scope."
|
||||
commands.allow = ["list_pools"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-list-pools"
|
||||
description = "Denies the list_pools command without any pre-configured scope."
|
||||
commands.deny = ["list_pools"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-list-projects"
|
||||
description = "Enables the list_projects command without any pre-configured scope."
|
||||
commands.allow = ["list_projects"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-list-projects"
|
||||
description = "Denies the list_projects command without any pre-configured scope."
|
||||
commands.deny = ["list_projects"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-oauth-login"
|
||||
description = "Enables the oauth_login command without any pre-configured scope."
|
||||
commands.allow = ["oauth_login"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-oauth-login"
|
||||
description = "Denies the oauth_login command without any pre-configured scope."
|
||||
commands.deny = ["oauth_login"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-open-main-window"
|
||||
description = "Enables the open_main_window command without any pre-configured scope."
|
||||
commands.allow = ["open_main_window"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-open-main-window"
|
||||
description = "Denies the open_main_window command without any pre-configured scope."
|
||||
commands.deny = ["open_main_window"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-open-panel-window"
|
||||
description = "Enables the open_panel_window command without any pre-configured scope."
|
||||
commands.allow = ["open_panel_window"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-open-panel-window"
|
||||
description = "Denies the open_panel_window command without any pre-configured scope."
|
||||
commands.deny = ["open_panel_window"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-open-terminal"
|
||||
description = "Enables the open_terminal command without any pre-configured scope."
|
||||
commands.allow = ["open_terminal"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-open-terminal"
|
||||
description = "Denies the open_terminal command without any pre-configured scope."
|
||||
commands.deny = ["open_terminal"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-panel-archive-cmd"
|
||||
description = "Enables the panel_archive_cmd command without any pre-configured scope."
|
||||
commands.allow = ["panel_archive_cmd"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-panel-archive-cmd"
|
||||
description = "Denies the panel_archive_cmd command without any pre-configured scope."
|
||||
commands.deny = ["panel_archive_cmd"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-panel-archive-dir-cmd"
|
||||
description = "Enables the panel_archive_dir_cmd command without any pre-configured scope."
|
||||
commands.allow = ["panel_archive_dir_cmd"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-panel-archive-dir-cmd"
|
||||
description = "Denies the panel_archive_dir_cmd command without any pre-configured scope."
|
||||
commands.deny = ["panel_archive_dir_cmd"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-panel-load"
|
||||
description = "Enables the panel_load command without any pre-configured scope."
|
||||
commands.allow = ["panel_load"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-panel-load"
|
||||
description = "Denies the panel_load command without any pre-configured scope."
|
||||
commands.deny = ["panel_load"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-panel-read"
|
||||
description = "Enables the panel_read command without any pre-configured scope."
|
||||
commands.allow = ["panel_read"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-panel-read"
|
||||
description = "Denies the panel_read command without any pre-configured scope."
|
||||
commands.deny = ["panel_read"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-panel-set"
|
||||
description = "Enables the panel_set command without any pre-configured scope."
|
||||
commands.allow = ["panel_set"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-panel-set"
|
||||
description = "Denies the panel_set command without any pre-configured scope."
|
||||
commands.deny = ["panel_set"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-pool-label"
|
||||
description = "Enables the pool_label command without any pre-configured scope."
|
||||
commands.allow = ["pool_label"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-pool-label"
|
||||
description = "Denies the pool_label command without any pre-configured scope."
|
||||
commands.deny = ["pool_label"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-project-icon"
|
||||
description = "Enables the project_icon command without any pre-configured scope."
|
||||
commands.allow = ["project_icon"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-project-icon"
|
||||
description = "Denies the project_icon command without any pre-configured scope."
|
||||
commands.deny = ["project_icon"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-project-work-dirs"
|
||||
description = "Enables the project_work_dirs command without any pre-configured scope."
|
||||
commands.allow = ["project_work_dirs"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-project-work-dirs"
|
||||
description = "Denies the project_work_dirs command without any pre-configured scope."
|
||||
commands.deny = ["project_work_dirs"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-quit-app"
|
||||
description = "Enables the quit_app command without any pre-configured scope."
|
||||
commands.allow = ["quit_app"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-quit-app"
|
||||
description = "Denies the quit_app command without any pre-configured scope."
|
||||
commands.deny = ["quit_app"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-remove-project"
|
||||
description = "Enables the remove_project command without any pre-configured scope."
|
||||
commands.allow = ["remove_project"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-remove-project"
|
||||
description = "Denies the remove_project command without any pre-configured scope."
|
||||
commands.deny = ["remove_project"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-remove-work-dir"
|
||||
description = "Enables the remove_work_dir command without any pre-configured scope."
|
||||
commands.allow = ["remove_work_dir"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-remove-work-dir"
|
||||
description = "Denies the remove_work_dir command without any pre-configured scope."
|
||||
commands.deny = ["remove_work_dir"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-rename-pool"
|
||||
description = "Enables the rename_pool command without any pre-configured scope."
|
||||
commands.allow = ["rename_pool"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-rename-pool"
|
||||
description = "Denies the rename_pool command without any pre-configured scope."
|
||||
commands.deny = ["rename_pool"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-restart-project"
|
||||
description = "Enables the restart_project command without any pre-configured scope."
|
||||
commands.allow = ["restart_project"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-restart-project"
|
||||
description = "Denies the restart_project command without any pre-configured scope."
|
||||
commands.deny = ["restart_project"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-reveal-path-cmd"
|
||||
description = "Enables the reveal_path_cmd command without any pre-configured scope."
|
||||
commands.allow = ["reveal_path_cmd"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-reveal-path-cmd"
|
||||
description = "Denies the reveal_path_cmd command without any pre-configured scope."
|
||||
commands.deny = ["reveal_path_cmd"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-search-read"
|
||||
description = "Enables the search_read command without any pre-configured scope."
|
||||
commands.allow = ["search_read"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-search-read"
|
||||
description = "Denies the search_read command without any pre-configured scope."
|
||||
commands.deny = ["search_read"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-search-run"
|
||||
description = "Enables the search_run command without any pre-configured scope."
|
||||
commands.allow = ["search_run"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-search-run"
|
||||
description = "Denies the search_run command without any pre-configured scope."
|
||||
commands.deny = ["search_run"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-apikey"
|
||||
description = "Enables the set_apikey command without any pre-configured scope."
|
||||
commands.allow = ["set_apikey"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-apikey"
|
||||
description = "Denies the set_apikey command without any pre-configured scope."
|
||||
commands.deny = ["set_apikey"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-archive-home-cmd"
|
||||
description = "Enables the set_archive_home_cmd command without any pre-configured scope."
|
||||
commands.allow = ["set_archive_home_cmd"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-archive-home-cmd"
|
||||
description = "Denies the set_archive_home_cmd command without any pre-configured scope."
|
||||
commands.deny = ["set_archive_home_cmd"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-project-dir"
|
||||
description = "Enables the set_project_dir command without any pre-configured scope."
|
||||
commands.allow = ["set_project_dir"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-project-dir"
|
||||
description = "Denies the set_project_dir command without any pre-configured scope."
|
||||
commands.deny = ["set_project_dir"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-sync-setting"
|
||||
description = "Enables the set_sync_setting command without any pre-configured scope."
|
||||
commands.allow = ["set_sync_setting"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-sync-setting"
|
||||
description = "Denies the set_sync_setting command without any pre-configured scope."
|
||||
commands.deny = ["set_sync_setting"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-terminal-config"
|
||||
description = "Enables the set_terminal_config command without any pre-configured scope."
|
||||
commands.allow = ["set_terminal_config"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-terminal-config"
|
||||
description = "Denies the set_terminal_config command without any pre-configured scope."
|
||||
commands.deny = ["set_terminal_config"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-terminal-font-size"
|
||||
description = "Enables the set_terminal_font_size command without any pre-configured scope."
|
||||
commands.allow = ["set_terminal_font_size"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-terminal-font-size"
|
||||
description = "Denies the set_terminal_font_size command without any pre-configured scope."
|
||||
commands.deny = ["set_terminal_font_size"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-todo"
|
||||
description = "Enables the set_todo command without any pre-configured scope."
|
||||
commands.allow = ["set_todo"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-todo"
|
||||
description = "Denies the set_todo command without any pre-configured scope."
|
||||
commands.deny = ["set_todo"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-spellcheck-lang"
|
||||
description = "Enables the spellcheck_lang command without any pre-configured scope."
|
||||
commands.allow = ["spellcheck_lang"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-spellcheck-lang"
|
||||
description = "Denies the spellcheck_lang command without any pre-configured scope."
|
||||
commands.deny = ["spellcheck_lang"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-start-or-focus-cmd"
|
||||
description = "Enables the start_or_focus_cmd command without any pre-configured scope."
|
||||
commands.allow = ["start_or_focus_cmd"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-start-or-focus-cmd"
|
||||
description = "Denies the start_or_focus_cmd command without any pre-configured scope."
|
||||
commands.deny = ["start_or_focus_cmd"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-stop-project"
|
||||
description = "Enables the stop_project command without any pre-configured scope."
|
||||
commands.allow = ["stop_project"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-stop-project"
|
||||
description = "Denies the stop_project command without any pre-configured scope."
|
||||
commands.deny = ["stop_project"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-sync-setting"
|
||||
description = "Enables the sync_setting command without any pre-configured scope."
|
||||
commands.allow = ["sync_setting"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-sync-setting"
|
||||
description = "Denies the sync_setting command without any pre-configured scope."
|
||||
commands.deny = ["sync_setting"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-term-log"
|
||||
description = "Enables the term_log command without any pre-configured scope."
|
||||
commands.allow = ["term_log"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-term-log"
|
||||
description = "Denies the term_log command without any pre-configured scope."
|
||||
commands.deny = ["term_log"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-term-resize"
|
||||
description = "Enables the term_resize command without any pre-configured scope."
|
||||
commands.allow = ["term_resize"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-term-resize"
|
||||
description = "Denies the term_resize command without any pre-configured scope."
|
||||
commands.deny = ["term_resize"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-term-start"
|
||||
description = "Enables the term_start command without any pre-configured scope."
|
||||
commands.allow = ["term_start"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-term-start"
|
||||
description = "Denies the term_start command without any pre-configured scope."
|
||||
commands.deny = ["term_start"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-term-write"
|
||||
description = "Enables the term_write command without any pre-configured scope."
|
||||
commands.allow = ["term_write"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-term-write"
|
||||
description = "Denies the term_write command without any pre-configured scope."
|
||||
commands.deny = ["term_write"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-terminal-font-size"
|
||||
description = "Enables the terminal_font_size command without any pre-configured scope."
|
||||
commands.allow = ["terminal_font_size"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-terminal-font-size"
|
||||
description = "Denies the terminal_font_size command without any pre-configured scope."
|
||||
commands.deny = ["terminal_font_size"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-todo-state"
|
||||
description = "Enables the todo_state command without any pre-configured scope."
|
||||
commands.allow = ["todo_state"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-todo-state"
|
||||
description = "Denies the todo_state command without any pre-configured scope."
|
||||
commands.deny = ["todo_state"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-unassign-pool"
|
||||
description = "Enables the unassign_pool command without any pre-configured scope."
|
||||
commands.allow = ["unassign_pool"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-unassign-pool"
|
||||
description = "Denies the unassign_pool command without any pre-configured scope."
|
||||
commands.deny = ["unassign_pool"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-usage-stats"
|
||||
description = "Enables the usage_stats command without any pre-configured scope."
|
||||
commands.allow = ["usage_stats"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-usage-stats"
|
||||
description = "Denies the usage_stats command without any pre-configured scope."
|
||||
commands.deny = ["usage_stats"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-wiki-open"
|
||||
description = "Enables the wiki_open command without any pre-configured scope."
|
||||
commands.allow = ["wiki_open"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-wiki-open"
|
||||
description = "Denies the wiki_open command without any pre-configured scope."
|
||||
commands.deny = ["wiki_open"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-wiki-read"
|
||||
description = "Enables the wiki_read command without any pre-configured scope."
|
||||
commands.allow = ["wiki_read"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-wiki-read"
|
||||
description = "Denies the wiki_read command without any pre-configured scope."
|
||||
commands.deny = ["wiki_read"]
|
||||
@@ -247,8 +247,8 @@ fn invoke_handlers() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send +
|
||||
commands::list_projects,
|
||||
commands::create_project_full,
|
||||
commands::add_project,
|
||||
commands::remove_project,
|
||||
commands::delete_project,
|
||||
commands::delete_preview,
|
||||
commands::delete_project_scoped,
|
||||
commands::project_work_dirs,
|
||||
commands::set_project_dir,
|
||||
commands::add_work_dir,
|
||||
|
||||
+20
-17
@@ -13,12 +13,12 @@ use crate::domain::pool::{
|
||||
link_pool_runtime_in, list_pools_in, read_pool, rename_pool_in, set_apikey_in, PoolInfo,
|
||||
};
|
||||
use crate::domain::project::{
|
||||
add_project_in, add_work_dir_in, assign_pool_in, create_project_full_in, delete_project_in,
|
||||
display_name_in, is_running, kill_terminals, list_projects_in, project_work_dirs_in,
|
||||
read_project_config_in, remove_work_dir_in, resolve_icon_path, running_projects_using_pool,
|
||||
set_project_dir_in, set_terminal_config_in, unassign_pool_in, Project, TerminalConfig,
|
||||
add_project_in, add_work_dir_in, assign_pool_in, create_project_full_in, delete_preview_in,
|
||||
delete_project_scoped_in, display_name_in, is_running, kill_terminals, list_projects_in,
|
||||
project_work_dirs_in, read_project_config_in, remove_work_dir_in, resolve_icon_path,
|
||||
running_projects_using_pool, set_project_dir_in, set_terminal_config_in, unassign_pool_in,
|
||||
DeletePreview, Project, TerminalConfig,
|
||||
};
|
||||
use crate::domain::registry::unregister_project;
|
||||
use crate::domain::settings;
|
||||
use crate::domain::todo::{set_todo_in, todo_state_in};
|
||||
use crate::domain::usage::{usage_stats_in, UsageRow};
|
||||
@@ -60,16 +60,6 @@ pub(crate) fn add_project(path: String) -> Result<(), String> {
|
||||
add_project_in(&Paths::real(), &path)
|
||||
}
|
||||
|
||||
/// Projekt aus der Registry nehmen; der Ordner bleibt unangetastet.
|
||||
#[tauri::command]
|
||||
pub(crate) fn remove_project(project: String) -> Result<(), String> {
|
||||
if is_running(&project) {
|
||||
let name = display_name_in(&Paths::real(), &project)?;
|
||||
return Err(format!("{name} läuft noch — erst beenden"));
|
||||
}
|
||||
unregister_project(&Paths::real(), &project)
|
||||
}
|
||||
|
||||
/// Projektordner neu zuordnen; bei laufender Session gesperrt.
|
||||
#[tauri::command]
|
||||
pub(crate) fn set_project_dir(project: String, dir: String) -> Result<(), String> {
|
||||
@@ -90,13 +80,26 @@ pub(crate) fn remove_work_dir(project: String, dir: String) -> Result<(), String
|
||||
remove_work_dir_in(&Paths::real(), &project, &dir)
|
||||
}
|
||||
|
||||
/// Artefakt-Vorschau für den Lösch-Dialog.
|
||||
#[tauri::command]
|
||||
pub(crate) fn delete_project(project: String, delete_work_dirs: bool) -> Result<(), String> {
|
||||
pub(crate) fn delete_preview(project: String) -> Result<DeletePreview, String> {
|
||||
delete_preview_in(&Paths::real(), &project)
|
||||
}
|
||||
|
||||
/// Löschen in drei Stufen: "integration" (nur ai-control-Spuren),
|
||||
/// "archive" (zusätzlich Archiv), "full" (zusätzlich Projektordner,
|
||||
/// Arbeitsordner per Flag).
|
||||
#[tauri::command]
|
||||
pub(crate) fn delete_project_scoped(
|
||||
project: String,
|
||||
scope: String,
|
||||
delete_work_dirs: bool,
|
||||
) -> Result<(), String> {
|
||||
if is_running(&project) {
|
||||
let name = display_name_in(&Paths::real(), &project)?;
|
||||
return Err(format!("{name} läuft noch — erst beenden"));
|
||||
}
|
||||
delete_project_in(&Paths::real(), &project, delete_work_dirs)
|
||||
delete_project_scoped_in(&Paths::real(), &project, &scope, delete_work_dirs)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -108,8 +108,13 @@ pub(crate) fn clear_project_archive_home(project: &str) -> Result<(), String> {
|
||||
remove_archive_permission(&paths, project, &dir)
|
||||
}
|
||||
|
||||
/// Gegenstück zu add_archive_permission.
|
||||
fn remove_archive_permission(paths: &Paths, project: &str, dir: &str) -> Result<(), String> {
|
||||
/// Gegenstück zu add_archive_permission; auch der Lösch-Dialog (Stufe „nur
|
||||
/// Integration") nimmt darüber die Archiv-Rechte zurück.
|
||||
pub(crate) fn remove_archive_permission(
|
||||
paths: &Paths,
|
||||
project: &str,
|
||||
dir: &str,
|
||||
) -> Result<(), String> {
|
||||
let sp = settings_path(&project_dir(paths, project)?);
|
||||
if !sp.is_file() {
|
||||
return Ok(());
|
||||
@@ -175,29 +180,42 @@ pub(crate) fn archive_panel_content(
|
||||
.as_secs();
|
||||
let (stamp, iso) = utc_stamp(secs);
|
||||
let title = first_line(&text);
|
||||
let path = free_path(&dir, &stamp, &slugify(&title));
|
||||
let (path, mut file) = create_unique(&dir, &stamp, &slugify(&title))?;
|
||||
// Frontmatter trägt den Anzeigenamen, nicht die Projekt-ID.
|
||||
let name = crate::domain::project::display_name_in(&Paths::real(), project)?;
|
||||
let doc = format!("{}{}\n", frontmatter(&title, &name, &iso, meta), text.trim_end());
|
||||
fs::write(&path, doc).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
std::io::Write::write_all(&mut file, doc.as_bytes())
|
||||
.map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Freier Dateiname für das Archiv-Dokument.
|
||||
/// Archiv-Dokument kollisionsfrei anlegen.
|
||||
///
|
||||
/// Der Zeitstempel hat Minutenauflösung; zweimal Archivieren innerhalb einer
|
||||
/// Minute mit derselben Titelzeile ergäbe denselben Namen. Da mit `fs::write`
|
||||
/// geschrieben wird, wäre das stiller Datenverlust — ausgerechnet im Archiv,
|
||||
/// das die dauerhafte Ablage ist. Darum bei Kollision `-2`, `-3`, … anhängen.
|
||||
fn free_path(dir: &std::path::Path, stamp: &str, slug: &str) -> PathBuf {
|
||||
let first = dir.join(format!("{stamp}-{slug}.md"));
|
||||
if !first.exists() {
|
||||
return first;
|
||||
/// Minute mit derselben Titelzeile ergäbe denselben Namen — bei Kollision
|
||||
/// wird `-2`, `-3`, … angehängt. Die Garantie kommt vom Dateisystem
|
||||
/// (`create_new`), nicht von einem `exists()`-Vorabblick: Beim Archiv-Sync
|
||||
/// über zwei Maschinen wäre der Vorabblick ein TOCTOU-Fenster, und stiller
|
||||
/// Datenverlust träfe ausgerechnet die dauerhafte Ablage.
|
||||
fn create_unique(
|
||||
dir: &std::path::Path,
|
||||
stamp: &str,
|
||||
slug: &str,
|
||||
) -> Result<(PathBuf, fs::File), String> {
|
||||
for n in 1.. {
|
||||
let name = if n == 1 {
|
||||
format!("{stamp}-{slug}.md")
|
||||
} else {
|
||||
format!("{stamp}-{slug}-{n}.md")
|
||||
};
|
||||
let path = dir.join(name);
|
||||
match fs::OpenOptions::new().write(true).create_new(true).open(&path) {
|
||||
Ok(file) => return Ok((path, file)),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
|
||||
Err(e) => return Err(format!("{}: {e}", path.display())),
|
||||
}
|
||||
(2..)
|
||||
.map(|n| dir.join(format!("{stamp}-{slug}-{n}.md")))
|
||||
.find(|p| !p.exists())
|
||||
.expect("unendlicher Zahlenraum")
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
/// Unterordner-Pfad: relativ, nur normale Komponenten (kein `..`, kein Root).
|
||||
@@ -377,21 +395,22 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Zweimal Archivieren in derselben Minute mit gleichem Titel darf die erste
|
||||
/// Datei nicht überschreiben — der Stempel hat nur Minutenauflösung.
|
||||
/// Datei nicht überschreiben — der Stempel hat nur Minutenauflösung, und
|
||||
/// die Datei entsteht bereits beim Anlegen (create_new), nicht erst beim
|
||||
/// Schreiben.
|
||||
#[test]
|
||||
fn gleicher_stempel_und_titel_kollidiert_nicht() {
|
||||
let dir = crate::domain::testutil::tmp_paths().home.join("archiv");
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
let erste = free_path(&dir, "2026-07-19_2118", "notiz");
|
||||
let (erste, mut f1) = create_unique(&dir, "2026-07-19_2118", "notiz").unwrap();
|
||||
assert_eq!(erste.file_name().unwrap(), "2026-07-19_2118-notiz.md");
|
||||
fs::write(&erste, "alt").unwrap();
|
||||
std::io::Write::write_all(&mut f1, b"alt").unwrap();
|
||||
|
||||
let zweite = free_path(&dir, "2026-07-19_2118", "notiz");
|
||||
let (zweite, _f2) = create_unique(&dir, "2026-07-19_2118", "notiz").unwrap();
|
||||
assert_eq!(zweite.file_name().unwrap(), "2026-07-19_2118-notiz-2.md");
|
||||
fs::write(&zweite, "neu").unwrap();
|
||||
|
||||
assert_eq!(fs::read_to_string(&erste).unwrap(), "alt");
|
||||
let dritte = free_path(&dir, "2026-07-19_2118", "notiz");
|
||||
let (dritte, _f3) = create_unique(&dir, "2026-07-19_2118", "notiz").unwrap();
|
||||
assert_eq!(dritte.file_name().unwrap(), "2026-07-19_2118-notiz-3.md");
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@ pub(crate) struct Pool {
|
||||
pub(crate) name: String,
|
||||
#[serde(rename = "credentialType")]
|
||||
pub(crate) credential_type: String,
|
||||
/// Unbekannte Keys unverändert durchreichen — beim Umbenennen wird die
|
||||
/// ganze pool.json neu geschrieben (dieselbe Fehlerklasse wie ProjectConfig).
|
||||
#[serde(flatten)]
|
||||
pub(crate) rest: serde_json::Map<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -136,13 +140,9 @@ fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
|
||||
Ok(paths.pool_dir(&uuid::Uuid::new_v4().to_string()))
|
||||
}
|
||||
|
||||
fn write_pool_json(dir: &PathBuf, name: &str, credential_type: &str) -> Result<(), String> {
|
||||
fn write_pool_json(dir: &PathBuf, pool: &Pool) -> Result<(), String> {
|
||||
fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
|
||||
let pool = Pool {
|
||||
name: name.to_string(),
|
||||
credential_type: credential_type.to_string(),
|
||||
};
|
||||
let raw = serde_json::to_string_pretty(&pool).map_err(|e| e.to_string())?;
|
||||
let raw = serde_json::to_string_pretty(pool).map_err(|e| e.to_string())?;
|
||||
crate::domain::write_atomic(&dir.join(POOL_FILE), &(raw + "\n"))
|
||||
}
|
||||
|
||||
@@ -381,7 +381,7 @@ pub(crate) fn create_apikey_pool_in(
|
||||
&dir,
|
||||
serde_json::json!({ "apiKeyHelper": crate::platform::apikey_helper_command(&dir, &id) }),
|
||||
)?;
|
||||
write_pool_json(&dir, name, "apikey")?;
|
||||
write_pool_json(&dir, &Pool { name: name.to_string(), credential_type: "apikey".into(), rest: Default::default() })?;
|
||||
if pool_sync_dir(paths).is_some() {
|
||||
link_pool_runtime_in(paths, &id)?;
|
||||
}
|
||||
@@ -395,7 +395,7 @@ pub(crate) fn create_apikey_pool_in(
|
||||
pub(crate) fn create_oauth_pool_in(paths: &Paths, name: &str) -> Result<String, String> {
|
||||
let dir = check_new_pool(paths, name)?;
|
||||
init_pool_config(&dir, serde_json::json!({}))?;
|
||||
write_pool_json(&dir, name, "oauth")?;
|
||||
write_pool_json(&dir, &Pool { name: name.to_string(), credential_type: "oauth".into(), rest: Default::default() })?;
|
||||
let id = dir.file_name().unwrap().to_string_lossy().into_owned();
|
||||
if pool_sync_dir(paths).is_some() {
|
||||
link_pool_runtime_in(paths, &id)?;
|
||||
@@ -407,11 +407,12 @@ pub(crate) fn create_oauth_pool_in(paths: &Paths, name: &str) -> Result<String,
|
||||
/// (und damit Keychain-Suffix, Symlinks, Zuordnungen) bleiben unverändert.
|
||||
pub(crate) fn rename_pool_in(paths: &Paths, pool: &str, name: &str) -> Result<(), String> {
|
||||
check_name(name)?;
|
||||
let current = read_pool(paths, pool)?;
|
||||
let mut current = read_pool(paths, pool)?;
|
||||
if pool_names(paths)?.iter().any(|(id, n)| id != pool && n == name) {
|
||||
return Err(format!("Pool existiert bereits: {name}"));
|
||||
}
|
||||
write_pool_json(&paths.pool_dir(pool), name, ¤t.credential_type)
|
||||
current.name = name.to_string();
|
||||
write_pool_json(&paths.pool_dir(pool), ¤t)
|
||||
}
|
||||
|
||||
/// Löscht einen Pool samt Ordner (inkl. Credentials, bei apikey auch den
|
||||
|
||||
+176
-15
@@ -499,27 +499,125 @@ pub(crate) fn project_work_dirs_in(paths: &Paths, name: &str) -> Result<Vec<Stri
|
||||
)
|
||||
}
|
||||
|
||||
/// Löscht den Projektordner und den Registry-Eintrag; optional auch die
|
||||
/// verknüpften Arbeitsordner. Bei laufender Session wird abgebrochen.
|
||||
pub(crate) fn delete_project_in(
|
||||
/// Vorschau für den Lösch-Dialog: welche Artefakte die drei Stufen
|
||||
/// (Integration / Integration+Archiv / komplett) konkret treffen würden.
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct DeletePreview {
|
||||
pub(crate) name: String,
|
||||
pub(crate) project_dir: String,
|
||||
pub(crate) ai_control_dir: bool,
|
||||
pub(crate) archive_permission: bool,
|
||||
pub(crate) todo_hook: bool,
|
||||
pub(crate) panel_files: usize,
|
||||
pub(crate) archive_home: Option<String>,
|
||||
pub(crate) archive_docs: usize,
|
||||
pub(crate) work_dirs: Vec<String>,
|
||||
}
|
||||
|
||||
pub(crate) fn delete_preview_in(paths: &Paths, project: &str) -> Result<DeletePreview, String> {
|
||||
check_name(project)?;
|
||||
let dir = project_dir(paths, project)?;
|
||||
let cfg = read_project_config_in(paths, project)?;
|
||||
let archive_docs = match cfg.archive_home.as_deref() {
|
||||
Some(a) => {
|
||||
let home = expand_home(paths, a);
|
||||
if home.is_dir() {
|
||||
crate::domain::archive_index::scan_archive(&home)?.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
Ok(DeletePreview {
|
||||
name: cfg.name.clone().unwrap_or_else(|| project.to_string()),
|
||||
project_dir: contract_home(paths, &dir),
|
||||
ai_control_dir: dir.join(PROJECT_CONFIG_DIR).is_dir(),
|
||||
archive_permission: cfg.archive_home.is_some(),
|
||||
todo_hook: crate::domain::todo::todo_state_in(paths, project)?,
|
||||
panel_files: session_files(paths, project).iter().filter(|f| f.is_file()).count(),
|
||||
archive_home: cfg.archive_home.clone(),
|
||||
archive_docs,
|
||||
work_dirs: project_work_dirs_in(paths, project)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Flüchtige Panel-Kanaldateien des Projekts unter ~/.config/ai-control/panels.
|
||||
fn session_files(paths: &Paths, project: &str) -> [PathBuf; 4] {
|
||||
let panels = paths.config_dir().join("panels");
|
||||
[
|
||||
panels.join(format!("{project}.md")),
|
||||
panels.join(format!("{project}.commands.jsonl")),
|
||||
panels.join(format!("{project}.search.json")),
|
||||
panels.join(format!("{project}.wiki.json")),
|
||||
]
|
||||
}
|
||||
|
||||
/// Löscht ein Projekt in drei Stufen (Eskalationsleiter, jede schließt die
|
||||
/// vorige ein):
|
||||
/// - "integration": nur die ai-control-Spuren — Registry, .ai-control/,
|
||||
/// Archiv-Rechte und Todo-Hook in der settings.json, Panel-Dateien,
|
||||
/// .desktop. Ordner, memory/ und das Claude-Code-Grundgerüst der
|
||||
/// settings.json bleiben.
|
||||
/// - "archive": zusätzlich der Archiv-Ordner samt Dokumenten.
|
||||
/// - "full": zusätzlich der Projektordner; Arbeitsordner per Flag.
|
||||
pub(crate) fn delete_project_scoped_in(
|
||||
paths: &Paths,
|
||||
name: &str,
|
||||
project: &str,
|
||||
scope: &str,
|
||||
delete_work_dirs: bool,
|
||||
) -> Result<(), String> {
|
||||
check_name(name)?;
|
||||
let dir = project_dir(paths, name)?;
|
||||
check_name(project)?;
|
||||
let dir = project_dir(paths, project)?;
|
||||
if !dir.is_dir() {
|
||||
return Err(format!("Projekt nicht gefunden: {name}"));
|
||||
return Err(format!("Projektordner nicht gefunden: {}", dir.display()));
|
||||
}
|
||||
let cfg = read_project_config_in(paths, project)?;
|
||||
|
||||
// Archiv vor der Integration räumen — Stufe 1 löscht die Config, die den
|
||||
// Pfad kennt.
|
||||
if scope == "archive" || scope == "full" {
|
||||
if let Some(a) = cfg.archive_home.as_deref() {
|
||||
let home = expand_home(paths, a);
|
||||
if home.is_dir() {
|
||||
fs::remove_dir_all(&home).map_err(|e| format!("{}: {e}", home.display()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match scope {
|
||||
"integration" | "archive" => {
|
||||
if let Some(a) = cfg.archive_home.as_deref() {
|
||||
crate::domain::archive::remove_archive_permission(paths, project, a)?;
|
||||
}
|
||||
if crate::domain::todo::todo_state_in(paths, project)? {
|
||||
crate::domain::todo::set_todo_in(paths, project, false)?;
|
||||
}
|
||||
let ac = dir.join(PROJECT_CONFIG_DIR);
|
||||
if ac.is_dir() {
|
||||
fs::remove_dir_all(&ac).map_err(|e| format!("{}: {e}", ac.display()))?;
|
||||
}
|
||||
}
|
||||
"full" => {
|
||||
if delete_work_dirs {
|
||||
for wd in project_work_dirs_in(paths, name)? {
|
||||
for wd in project_work_dirs_in(paths, project)? {
|
||||
let wd_path = expand_home(paths, &wd);
|
||||
fs::remove_dir_all(&wd_path).map_err(|e| format!("{}: {e}", wd_path.display()))?;
|
||||
}
|
||||
}
|
||||
fs::remove_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
|
||||
crate::domain::registry::unregister_project(paths, name)?;
|
||||
crate::platform::remove_terminal_desktop(paths, name);
|
||||
}
|
||||
other => return Err(format!("unbekannter Lösch-Umfang: {other}")),
|
||||
}
|
||||
|
||||
for f in session_files(paths, project) {
|
||||
if f.is_file() {
|
||||
fs::remove_file(&f).map_err(|e| format!("{}: {e}", f.display()))?;
|
||||
}
|
||||
}
|
||||
crate::domain::registry::unregister_project(paths, project)?;
|
||||
crate::platform::remove_terminal_desktop(paths, project);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1149,7 +1247,7 @@ mod tests {
|
||||
fn projekt_loeschen() {
|
||||
let p = tmp_paths();
|
||||
create_project(&p, "proj").unwrap();
|
||||
delete_project_in(&p, "proj", false).unwrap();
|
||||
delete_project_scoped_in(&p, "proj", "full", false).unwrap();
|
||||
assert!(!p.projects_dir().join("proj").exists());
|
||||
}
|
||||
|
||||
@@ -1158,7 +1256,7 @@ mod tests {
|
||||
let p = tmp_paths();
|
||||
let id = create_project_full_in(&p, "proj", None, None, Some("~/projects/proj"), true, TerminalConfig::default(), false)
|
||||
.unwrap();
|
||||
delete_project_in(&p, &id, false).unwrap();
|
||||
delete_project_scoped_in(&p, &id, "full", false).unwrap();
|
||||
assert!(!p.projects_dir().join("proj").exists());
|
||||
assert!(p.home.join("projects").join("proj").is_dir());
|
||||
}
|
||||
@@ -1168,7 +1266,7 @@ mod tests {
|
||||
let p = tmp_paths();
|
||||
let id = create_project_full_in(&p, "proj", None, None, Some("~/projects/proj"), true, TerminalConfig::default(), false)
|
||||
.unwrap();
|
||||
delete_project_in(&p, &id, true).unwrap();
|
||||
delete_project_scoped_in(&p, &id, "full", true).unwrap();
|
||||
assert!(!p.projects_dir().join("proj").exists());
|
||||
assert!(!p.home.join("projects").join("proj").exists());
|
||||
}
|
||||
@@ -1179,7 +1277,7 @@ mod tests {
|
||||
let id = create_project_full_in(&p, "proj", None, None, Some("~/projects/proj"), true, TerminalConfig::default(), false)
|
||||
.unwrap();
|
||||
fs::remove_dir_all(p.home.join("projects").join("proj")).unwrap();
|
||||
assert!(delete_project_in(&p, &id, true).is_err());
|
||||
assert!(delete_project_scoped_in(&p, &id, "full", true).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1276,9 +1374,72 @@ mod tests {
|
||||
assert!(read_for_panel_in(&p, "proj", "/dev/zero").is_err());
|
||||
}
|
||||
|
||||
/// Stufe „nur Integration": ai-control-Spuren weg, Ordner und
|
||||
/// Claude-Code-Bestand bleiben.
|
||||
#[test]
|
||||
fn loeschen_nur_integration() {
|
||||
let p = tmp_paths();
|
||||
let id = create_project_full_in(&p, "proj", None, None, None, false, TerminalConfig::default(), true)
|
||||
.unwrap();
|
||||
let dir = p.projects_dir().join("proj");
|
||||
// Archiv-Home samt Permission wie über die UI gesetzt
|
||||
let mut cfg = read_project_config_in(&p, &id).unwrap();
|
||||
cfg.archive_home = Some("~/archiv/proj".into());
|
||||
write_project_config_in(&p, &id, &cfg).unwrap();
|
||||
crate::domain::archive::add_archive_permission(&p, &id, "~/archiv/proj").unwrap();
|
||||
fs::create_dir_all(p.home.join("archiv/proj")).unwrap();
|
||||
fs::write(p.home.join("archiv/proj/doc.md"), "inhalt").unwrap();
|
||||
// flüchtige Panel-Datei
|
||||
fs::create_dir_all(p.config_dir().join("panels")).unwrap();
|
||||
fs::write(p.config_dir().join("panels").join(format!("{id}.md")), "x").unwrap();
|
||||
|
||||
let preview = delete_preview_in(&p, &id).unwrap();
|
||||
assert!(preview.ai_control_dir && preview.archive_permission && preview.todo_hook);
|
||||
assert_eq!(preview.panel_files, 1);
|
||||
assert_eq!(preview.archive_docs, 1);
|
||||
|
||||
delete_project_scoped_in(&p, &id, "integration", false).unwrap();
|
||||
|
||||
assert!(dir.is_dir());
|
||||
assert!(!dir.join(PROJECT_CONFIG_DIR).exists());
|
||||
let sp = fs::read_to_string(settings_path(&dir)).unwrap();
|
||||
assert!(!sp.contains("archiv/proj"));
|
||||
assert!(!sp.contains("OFFENE-PUNKTE.md")); // Hook weg (leere Hook-Liste darf bleiben)
|
||||
assert!(sp.contains("autoMemoryDirectory")); // Claude-Code-Bestand bleibt
|
||||
assert!(dir.join(crate::domain::todo::TODO_FILE).is_file()); // Nutzerinhalt bleibt
|
||||
assert!(p.home.join("archiv/proj/doc.md").is_file()); // Archiv bleibt
|
||||
assert!(!p.config_dir().join("panels").join(format!("{id}.md")).exists());
|
||||
assert!(load_registry(&p).unwrap().is_empty());
|
||||
}
|
||||
|
||||
/// Stufe „Integration & Archiv": zusätzlich stirbt der Archiv-Ordner.
|
||||
#[test]
|
||||
fn loeschen_integration_und_archiv() {
|
||||
let p = tmp_paths();
|
||||
let id = create_project_full_in(&p, "proj", None, None, None, false, TerminalConfig::default(), false)
|
||||
.unwrap();
|
||||
let mut cfg = read_project_config_in(&p, &id).unwrap();
|
||||
cfg.archive_home = Some("~/archiv/proj".into());
|
||||
write_project_config_in(&p, &id, &cfg).unwrap();
|
||||
fs::create_dir_all(p.home.join("archiv/proj")).unwrap();
|
||||
|
||||
delete_project_scoped_in(&p, &id, "archive", false).unwrap();
|
||||
|
||||
assert!(p.projects_dir().join("proj").is_dir());
|
||||
assert!(!p.home.join("archiv/proj").exists());
|
||||
assert!(load_registry(&p).unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loeschen_unbekannter_umfang_scheitert() {
|
||||
let p = tmp_paths();
|
||||
create_project(&p, "proj").unwrap();
|
||||
assert!(delete_project_scoped_in(&p, "proj", "halb", false).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projekt_loeschen_unbekannt_scheitert() {
|
||||
let p = tmp_paths();
|
||||
assert!(delete_project_in(&p, "gibtsnicht", false).is_err());
|
||||
assert!(delete_project_scoped_in(&p, "gibtsnicht", "full", false).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,13 +62,22 @@ pub(crate) fn spellcheck_lang(paths: &Paths) -> String {
|
||||
}
|
||||
|
||||
/// Einen Schlüssel setzen; übrige App-settings bleiben erhalten.
|
||||
///
|
||||
/// Nur eine *fehlende* Datei rechtfertigt ein frisches Objekt. Unlesbar oder
|
||||
/// kaputtes JSON bricht ab — sonst würde ein einziges Verstellen der
|
||||
/// Schriftgröße claudeCommand, poolSyncDir & Co. endgültig verwerfen
|
||||
/// (read_app_settings wirft für die Lese-Getter alles in denselben None).
|
||||
fn write_app_setting(
|
||||
paths: &Paths,
|
||||
key: &str,
|
||||
value: serde_json::Value,
|
||||
) -> Result<(), String> {
|
||||
let path = paths.config_dir().join(APP_SETTINGS_FILE);
|
||||
let mut v = read_app_settings(paths).unwrap_or_else(|| serde_json::json!({}));
|
||||
let mut v: serde_json::Value = match fs::read_to_string(&path) {
|
||||
Ok(raw) => serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", path.display()))?,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => serde_json::json!({}),
|
||||
Err(e) => return Err(format!("{}: {e}", path.display())),
|
||||
};
|
||||
v[key] = value;
|
||||
fs::create_dir_all(paths.config_dir())
|
||||
.map_err(|e| format!("{}: {e}", paths.config_dir().display()))?;
|
||||
|
||||
+11
-1
@@ -276,7 +276,17 @@ fn call_write(req: &Value) -> Value {
|
||||
static SESSION_MARKED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
fn call_write_commands(req: &Value) -> Value {
|
||||
let commands = req["params"]["arguments"]["commands"].clone();
|
||||
// Jeder Eintrag bekommt beim Schreiben eine stabile ID — das Panel löscht
|
||||
// darüber, statt Positionen über Watcher-Latenz und Fenstergrenzen zu
|
||||
// reichen (der frühere Index+Text-Abgleich war ein Provisorium).
|
||||
let mut commands = req["params"]["arguments"]["commands"].clone();
|
||||
if let Some(arr) = commands.as_array_mut() {
|
||||
for c in arr {
|
||||
if let Some(obj) = c.as_object_mut() {
|
||||
obj.insert("id".into(), json!(uuid::Uuid::new_v4().to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
let count = commands.as_array().map(Vec::len).unwrap_or(0);
|
||||
let path = match env_path("AI_CONTROL_COMMANDS") {
|
||||
Ok(path) => path,
|
||||
|
||||
+15
-30
@@ -270,27 +270,12 @@ pub fn commands_read(project: String) -> String {
|
||||
}
|
||||
|
||||
/// Entfernt einen Befehl aus der Command-History (Löschen einer Kachel im
|
||||
/// Panel). `line` ist der Index der nicht-leeren JSONL-Zeile, `entry` der
|
||||
/// Index im commands-Array des Records; ein leer gewordener Record fällt mit
|
||||
/// weg. Der Watcher meldet den neuen Stand als `commands-update`.
|
||||
///
|
||||
/// `cmd` ist der angezeigte Befehlstext und wird vor dem Löschen abgeglichen:
|
||||
/// Die Indizes des Panels sind der Datei um bis zu ein Watcher-Intervall
|
||||
/// hinterher, und ein leer gewordener Record verschiebt alle folgenden. Ohne
|
||||
/// Abgleich löscht ein Doppelklick — oder das zweite Fenster auf derselben
|
||||
/// Liste — die falsche Kachel oder greift ins Leere.
|
||||
///
|
||||
/// Bewusst ein Provisorium, keine echte Identität: Zwei gleiche Befehle im
|
||||
/// selben Record sind so nicht unterscheidbar. Sauber wäre eine stabile ID, die
|
||||
/// beim Schreiben der JSONL vergeben wird, statt Positionen über die
|
||||
/// Fenstergrenze zu reichen.
|
||||
/// Panel) über seine stabile ID, die write_commands beim Schreiben vergibt.
|
||||
/// Ein leer gewordener Record fällt mit weg; der Watcher meldet den neuen
|
||||
/// Stand als `commands-update`. Doppelklick oder ein zweites Fenster auf
|
||||
/// derselben Liste laufen ins „bereits entfernt" statt auf falsche Indizes.
|
||||
#[tauri::command]
|
||||
pub fn commands_delete(
|
||||
project: String,
|
||||
line: usize,
|
||||
entry: usize,
|
||||
cmd: String,
|
||||
) -> Result<(), String> {
|
||||
pub fn commands_delete(project: String, id: String) -> Result<(), String> {
|
||||
let path = commands_file(&project);
|
||||
let text = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
||||
let mut records: Vec<serde_json::Value> = text
|
||||
@@ -298,18 +283,18 @@ pub fn commands_delete(
|
||||
.filter(|l| !l.trim().is_empty())
|
||||
.map(|l| serde_json::from_str(l).map_err(|e| e.to_string()))
|
||||
.collect::<Result<_, _>>()?;
|
||||
let cmds = records
|
||||
.get_mut(line)
|
||||
.ok_or("Befehl bereits entfernt")?["commands"]
|
||||
.as_array_mut()
|
||||
.ok_or("Record ohne commands")?;
|
||||
if cmds.get(entry).and_then(|c| c["cmd"].as_str()) != Some(cmd.as_str()) {
|
||||
let mut found = false;
|
||||
for rec in records.iter_mut() {
|
||||
if let Some(cmds) = rec["commands"].as_array_mut() {
|
||||
let before = cmds.len();
|
||||
cmds.retain(|c| c["id"].as_str() != Some(id.as_str()));
|
||||
found = found || cmds.len() != before;
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return Err("Befehl bereits entfernt".into());
|
||||
}
|
||||
cmds.remove(entry);
|
||||
if cmds.is_empty() {
|
||||
records.remove(line);
|
||||
}
|
||||
records.retain(|r| r["commands"].as_array().is_none_or(|c| !c.is_empty()));
|
||||
let mut out = String::new();
|
||||
for rec in &records {
|
||||
out.push_str(&rec.to_string());
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"macOSPrivateApi": true,
|
||||
"windows": [],
|
||||
"security": {
|
||||
"csp": null
|
||||
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src ipc: http://ipc.localhost",
|
||||
"devCsp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src ipc: http://ipc.localhost ws://localhost:1420 http://localhost:1420"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
|
||||
@@ -79,7 +79,7 @@ describe("initCommandsView", () => {
|
||||
JSON.stringify({ ts: 1700000000, session: true }),
|
||||
JSON.stringify({
|
||||
ts: 1700000100,
|
||||
commands: [{ cmd: "ls -la", note: "Liste" }, { cmd: "pwd" }],
|
||||
commands: [{ cmd: "ls -la", note: "Liste", id: "id-1" }, { cmd: "pwd", id: "id-2" }],
|
||||
}),
|
||||
].join("\n");
|
||||
|
||||
@@ -94,14 +94,14 @@ describe("initCommandsView", () => {
|
||||
expect(view.empty()).toBe(true);
|
||||
});
|
||||
|
||||
/// Der Befehlstext geht mit, weil die Indizes der Datei hinterherhinken
|
||||
/// können; das Backend gleicht damit ab, bevor es löscht.
|
||||
it("meldet Löschen mit Record-Index, Eintrags-Index und Befehl", () => {
|
||||
/// Gelöscht wird über die stabile ID aus write_commands — keine Indizes,
|
||||
/// die der Datei hinterherhinken könnten.
|
||||
it("meldet Löschen mit der Eintrags-ID", () => {
|
||||
const onDelete = vi.fn();
|
||||
const view = initCommandsView(document.getElementById("c")!, onDelete);
|
||||
view.set(jsonl);
|
||||
const dels = document.querySelectorAll<HTMLElement>(".cmd-del");
|
||||
dels[1].click(); // zweite Kachel im Record 1
|
||||
expect(onDelete).toHaveBeenCalledWith(1, 1, "pwd");
|
||||
expect(onDelete).toHaveBeenCalledWith("id-2");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/// Kachel-Ansicht der Command-History: rendert die JSONL-Datei
|
||||
/// (write_commands im MCP-Server) als kopierbare Kacheln, Neuestes oben,
|
||||
/// mit Zeitmarken und Session-Trennern. Löschen einer Kachel geht als
|
||||
/// onDelete(line, entry) an den Aufrufer; der neue Stand kommt über den
|
||||
/// onDelete(id) an den Aufrufer; der neue Stand kommt über den
|
||||
/// Watcher zurück. DOM wird per createElement gebaut — Befehle sind
|
||||
/// Fremdtext und gehen nie durch innerHTML.
|
||||
|
||||
@@ -17,6 +17,8 @@ function stripInvisibles(s: string): string {
|
||||
interface CommandEntry {
|
||||
cmd: string;
|
||||
note?: string;
|
||||
/// Stabile ID aus write_commands; Grundlage fürs Löschen.
|
||||
id?: string;
|
||||
}
|
||||
|
||||
interface Record {
|
||||
@@ -79,7 +81,7 @@ function deleteBtn(onClick: () => void): HTMLButtonElement {
|
||||
|
||||
export function initCommandsView(
|
||||
container: HTMLElement,
|
||||
onDelete: (line: number, entry: number, cmd: string) => void,
|
||||
onDelete: (id: string) => void,
|
||||
): CommandsView {
|
||||
let count = 0;
|
||||
|
||||
@@ -120,7 +122,7 @@ export function initCommandsView(
|
||||
}
|
||||
block.append(head);
|
||||
|
||||
cmds.forEach((entry, j) => {
|
||||
cmds.forEach((entry) => {
|
||||
// Unsichtbare Steuerzeichen (Bidi, Zero-Width) aus Anzeige UND
|
||||
// Kopie halten — sonst sieht der Nutzer einen anderen Befehl, als
|
||||
// die Zwischenablage enthält. Der Lösch-Abgleich läuft weiter über
|
||||
@@ -140,7 +142,7 @@ export function initCommandsView(
|
||||
note.textContent = entry.note;
|
||||
body.append(note);
|
||||
}
|
||||
tile.append(body, copyBtn(() => visible), deleteBtn(() => onDelete(i, j, entry.cmd)));
|
||||
tile.append(body, copyBtn(() => visible), deleteBtn(() => onDelete(entry.id ?? "")));
|
||||
block.append(tile);
|
||||
});
|
||||
container.append(block);
|
||||
|
||||
@@ -201,7 +201,8 @@ onMounted(refresh);
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
|
||||
<table v-if="pools.length" class="grid">
|
||||
<div v-if="pools.length" class="list-scroll">
|
||||
<table class="grid">
|
||||
<colgroup>
|
||||
<col />
|
||||
<col class="col-type" />
|
||||
@@ -268,6 +269,7 @@ onMounted(refresh);
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-else class="empty">{{ $t("pools.empty") }}</p>
|
||||
|
||||
<div v-if="dialog" class="overlay" @click.self="closeDialog">
|
||||
|
||||
+117
-35
@@ -379,10 +379,25 @@ async function createProject() {
|
||||
}
|
||||
}
|
||||
|
||||
interface DeletePreview {
|
||||
name: string;
|
||||
projectDir: string;
|
||||
aiControlDir: boolean;
|
||||
archivePermission: boolean;
|
||||
todoHook: boolean;
|
||||
panelFiles: number;
|
||||
archiveHome: string | null;
|
||||
archiveDocs: number;
|
||||
workDirs: string[];
|
||||
}
|
||||
|
||||
// Eskalationsleiter: jede Stufe schließt die vorige ein.
|
||||
type DeleteScope = "integration" | "archive" | "full";
|
||||
|
||||
interface PendingDelete {
|
||||
id: string;
|
||||
name: string;
|
||||
workDirs: string[];
|
||||
preview: DeletePreview;
|
||||
scope: DeleteScope;
|
||||
deleteWorkDirs: boolean;
|
||||
}
|
||||
|
||||
@@ -391,10 +406,8 @@ const pendingDelete = ref<PendingDelete | null>(null);
|
||||
async function askDelete(p: Project) {
|
||||
error.value = "";
|
||||
try {
|
||||
const workDirs = await invoke<string[]>("project_work_dirs", {
|
||||
project: p.id,
|
||||
});
|
||||
pendingDelete.value = { id: p.id, name: p.name, workDirs, deleteWorkDirs: false };
|
||||
const preview = await invoke<DeletePreview>("delete_preview", { project: p.id });
|
||||
pendingDelete.value = { id: p.id, preview, scope: "integration", deleteWorkDirs: false };
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
}
|
||||
@@ -403,9 +416,10 @@ async function askDelete(p: Project) {
|
||||
async function confirmDelete() {
|
||||
const d = pendingDelete.value!;
|
||||
try {
|
||||
await invoke("delete_project", {
|
||||
await invoke("delete_project_scoped", {
|
||||
project: d.id,
|
||||
deleteWorkDirs: d.deleteWorkDirs,
|
||||
scope: d.scope,
|
||||
deleteWorkDirs: d.scope === "full" && d.deleteWorkDirs,
|
||||
});
|
||||
pendingDelete.value = null;
|
||||
await refresh();
|
||||
@@ -415,19 +429,6 @@ async function confirmDelete() {
|
||||
}
|
||||
}
|
||||
|
||||
// Nur den Registry-Eintrag entfernen; der Ordner bleibt.
|
||||
async function unlinkProject() {
|
||||
const d = pendingDelete.value!;
|
||||
try {
|
||||
await invoke("remove_project", { project: d.id });
|
||||
pendingDelete.value = null;
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
pendingDelete.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
let timer: number;
|
||||
onMounted(() => {
|
||||
refresh();
|
||||
@@ -447,6 +448,7 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<div class="list-scroll">
|
||||
<table class="grid">
|
||||
<colgroup>
|
||||
<col class="col-dot" />
|
||||
@@ -516,6 +518,7 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-if="wizard" class="overlay" @click.self="wizard = null">
|
||||
<form class="dialog" @submit.prevent="createProject">
|
||||
@@ -619,30 +622,76 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
|
||||
<div v-if="pendingDelete" class="overlay" @click.self="pendingDelete = null">
|
||||
<div class="dialog">
|
||||
<h3>{{ $t("projects.deleteTitle", { name: pendingDelete.name }) }}</h3>
|
||||
<p class="hint">{{ $t("projects.deleteWarning", { name: pendingDelete.name }) }}</p>
|
||||
<template v-if="pendingDelete.workDirs.length">
|
||||
<label class="checkline">
|
||||
<h3>{{ $t("projects.deleteTitle", { name: pendingDelete.preview.name }) }}</h3>
|
||||
|
||||
<label class="scope">
|
||||
<input v-model="pendingDelete.scope" type="radio" value="integration" />
|
||||
<span>
|
||||
<strong>{{ $t("projects.scopeIntegration") }}</strong>
|
||||
<small>{{ $t("projects.scopeIntegrationDesc") }}</small>
|
||||
</span>
|
||||
</label>
|
||||
<label class="scope">
|
||||
<input
|
||||
v-model="pendingDelete.scope"
|
||||
type="radio"
|
||||
value="archive"
|
||||
:disabled="!pendingDelete.preview.archiveHome"
|
||||
/>
|
||||
<span>
|
||||
<strong>{{ $t("projects.scopeArchive") }}</strong>
|
||||
<small v-if="pendingDelete.preview.archiveHome">
|
||||
{{ $t("projects.scopeArchiveDesc", { path: pendingDelete.preview.archiveHome }) }}
|
||||
({{ $t("projects.docCount", pendingDelete.preview.archiveDocs) }})
|
||||
</small>
|
||||
<small v-else>{{ $t("projects.archiveNone") }}</small>
|
||||
</span>
|
||||
</label>
|
||||
<label class="scope">
|
||||
<input v-model="pendingDelete.scope" type="radio" value="full" />
|
||||
<span>
|
||||
<strong>{{ $t("projects.scopeFull") }}</strong>
|
||||
<small>{{ $t("projects.scopeFullDesc", { path: pendingDelete.preview.projectDir }) }}</small>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
v-if="pendingDelete.scope === 'full' && pendingDelete.preview.workDirs.length"
|
||||
class="checkline"
|
||||
>
|
||||
<input v-model="pendingDelete.deleteWorkDirs" type="checkbox" />
|
||||
{{ $t("projects.deleteWorkDirs") }}
|
||||
</label>
|
||||
|
||||
<p class="hint">{{ $t("projects.deletePreviewTitle") }}</p>
|
||||
<ul class="affected">
|
||||
<li v-for="wd in pendingDelete.workDirs" :key="wd">{{ wd }}</li>
|
||||
</ul>
|
||||
<p v-if="!pendingDelete.deleteWorkDirs" class="hint">
|
||||
{{ $t("projects.workDirsStay") }}
|
||||
</p>
|
||||
<li>{{ $t("projects.artRegistry") }}</li>
|
||||
<li v-if="pendingDelete.preview.aiControlDir">{{ $t("projects.artAiControl") }}</li>
|
||||
<li v-if="pendingDelete.preview.archivePermission">{{ $t("projects.artArchivePerm") }}</li>
|
||||
<li v-if="pendingDelete.preview.todoHook">{{ $t("projects.artTodoHook") }}</li>
|
||||
<li v-if="pendingDelete.preview.panelFiles">
|
||||
{{ $t("projects.artPanelFiles", pendingDelete.preview.panelFiles) }}
|
||||
</li>
|
||||
<li>{{ $t("projects.artDesktop") }}</li>
|
||||
<li v-if="pendingDelete.scope !== 'integration' && pendingDelete.preview.archiveHome">
|
||||
{{ $t("projects.artArchive", { path: pendingDelete.preview.archiveHome }) }}
|
||||
({{ $t("projects.docCount", pendingDelete.preview.archiveDocs) }})
|
||||
</li>
|
||||
<li v-if="pendingDelete.scope === 'full'">
|
||||
{{ $t("projects.artProjectDir", { path: pendingDelete.preview.projectDir }) }}
|
||||
</li>
|
||||
<template v-if="pendingDelete.scope === 'full' && pendingDelete.deleteWorkDirs">
|
||||
<li v-for="wd in pendingDelete.preview.workDirs" :key="wd">
|
||||
{{ $t("projects.artWorkDir", { path: wd }) }}
|
||||
</li>
|
||||
</template>
|
||||
<p class="hint">{{ $t("projects.unlinkHint") }}</p>
|
||||
</ul>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" @click="pendingDelete = null">
|
||||
{{ $t("projects.cancel") }}
|
||||
</button>
|
||||
<button @click="unlinkProject">
|
||||
{{ $t("projects.unlink") }}
|
||||
</button>
|
||||
<button class="danger" @click="confirmDelete">
|
||||
{{ $t("projects.delete") }}
|
||||
{{ $t("projects.deleteConfirm") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -784,6 +833,39 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Lösch-Dialog: drei Stufen als Radio-Zeilen mit Beschreibung. */
|
||||
.scope {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.6rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.scope input {
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.scope span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.scope strong {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.scope small {
|
||||
color: var(--overlay);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.scope:has(input:disabled) {
|
||||
opacity: 0.55;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Settings-Dialog: festes Grid Label | Wert | Aktionen in drei Gruppen. */
|
||||
.settings-dialog {
|
||||
position: relative;
|
||||
|
||||
@@ -113,7 +113,8 @@ function cost(n: number): string {
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
|
||||
<table v-if="groups.length" class="grid">
|
||||
<div v-if="groups.length" class="list-scroll">
|
||||
<table class="grid">
|
||||
<colgroup>
|
||||
<col />
|
||||
<col class="col-num" />
|
||||
@@ -164,5 +165,6 @@ function cost(n: number): string {
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<p v-else-if="!loading" class="empty">{{ $t("usage.empty") }}</p>
|
||||
</template>
|
||||
|
||||
+38
-12
@@ -71,13 +71,26 @@ const de = {
|
||||
delete: "Projekt löschen",
|
||||
deleteRunning: "Läuft noch — erst beenden",
|
||||
deleteTitle: "{name} löschen",
|
||||
deleteWarning:
|
||||
"Der Projektordner (inkl. memory/, Settings und Pool-Zuordnung) wird endgültig gelöscht.",
|
||||
deleteConfirm: "Entfernen",
|
||||
deleteWorkDirs: "Arbeitsordner mitlöschen:",
|
||||
workDirsStay: "Die Arbeitsordner bleiben unangetastet.",
|
||||
unlink: "Nur aus Liste nehmen",
|
||||
unlinkHint:
|
||||
"Nur aus Liste nehmen entfernt den Eintrag; der Ordner bleibt unangetastet.",
|
||||
scopeIntegration: "Nur Integration entfernen",
|
||||
scopeIntegrationDesc:
|
||||
"ai-control-Spuren werden entfernt; Projektordner, memory/ und Todoliste bleiben.",
|
||||
scopeArchive: "Integration & Archiv",
|
||||
scopeArchiveDesc: "zusätzlich {path} — endgültig.",
|
||||
scopeFull: "Projekt komplett löschen",
|
||||
scopeFullDesc: "zusätzlich der Projektordner {path} — endgültig.",
|
||||
deletePreviewTitle: "Wird entfernt:",
|
||||
docCount: "kein Dokument | 1 Dokument | {count} Dokumente",
|
||||
artRegistry: "Registry-Eintrag",
|
||||
artAiControl: ".ai-control/ (Config + Icon)",
|
||||
artArchivePerm: "Archiv-Berechtigung in .claude/settings.json",
|
||||
artTodoHook: "Todo-Hook in .claude/settings.json",
|
||||
artPanelFiles: "Panel-Kanaldatei | {count} Panel-Kanaldateien",
|
||||
artDesktop: ".desktop-Starter",
|
||||
artArchive: "Archiv {path}",
|
||||
artProjectDir: "Projektordner {path}",
|
||||
artWorkDir: "Arbeitsordner {path}",
|
||||
todo: "Todoliste",
|
||||
todoDesc: "OFFENE-PUNKTE.md bei jedem Sessionstart einspielen",
|
||||
groupAppearance: "Darstellung",
|
||||
@@ -201,13 +214,26 @@ const en: typeof de = {
|
||||
delete: "Delete project",
|
||||
deleteRunning: "Still running — stop it first",
|
||||
deleteTitle: "Delete {name}",
|
||||
deleteWarning:
|
||||
"The project folder (incl. memory/, settings and pool assignment) will be deleted permanently.",
|
||||
deleteConfirm: "Remove",
|
||||
deleteWorkDirs: "Also delete working folders:",
|
||||
workDirsStay: "The working folders stay untouched.",
|
||||
unlink: "Remove from list only",
|
||||
unlinkHint:
|
||||
"Remove from list only drops the entry; the folder stays untouched.",
|
||||
scopeIntegration: "Remove integration only",
|
||||
scopeIntegrationDesc:
|
||||
"ai-control traces are removed; project folder, memory/ and todo list stay.",
|
||||
scopeArchive: "Integration & archive",
|
||||
scopeArchiveDesc: "additionally {path} — permanent.",
|
||||
scopeFull: "Delete project completely",
|
||||
scopeFullDesc: "additionally the project folder {path} — permanent.",
|
||||
deletePreviewTitle: "Will be removed:",
|
||||
docCount: "no documents | 1 document | {count} documents",
|
||||
artRegistry: "registry entry",
|
||||
artAiControl: ".ai-control/ (config + icon)",
|
||||
artArchivePerm: "archive permission in .claude/settings.json",
|
||||
artTodoHook: "todo hook in .claude/settings.json",
|
||||
artPanelFiles: "panel channel file | {count} panel channel files",
|
||||
artDesktop: ".desktop launcher",
|
||||
artArchive: "archive {path}",
|
||||
artProjectDir: "project folder {path}",
|
||||
artWorkDir: "working folder {path}",
|
||||
todo: "Todo list",
|
||||
todoDesc: "Inject OFFENE-PUNKTE.md at every session start",
|
||||
groupAppearance: "Appearance",
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ export async function wirePanel(
|
||||
});
|
||||
const cmdView = initCommandsView(
|
||||
document.getElementById("commands-content")!,
|
||||
(line, entry, cmd) => invoke("commands_delete", { project, line, entry, cmd }),
|
||||
(id) => invoke("commands_delete", { project, id }),
|
||||
);
|
||||
// Treffer-Klick lädt das Dokument in den Dokument-Tab (dort editier- und
|
||||
// archivierbar); der Sprung ins Wiki geht von dort aus.
|
||||
|
||||
+11
-1
@@ -123,10 +123,20 @@ input[type="checkbox"] {
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Tab-Inhalte: Toolbar und Fehlerzeile stehen, nur die Liste scrollt. */
|
||||
.list-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* ---------- Controls ---------- */
|
||||
|
||||
button,
|
||||
|
||||
Reference in New Issue
Block a user