pyquipu-cli 0.3.2__tar.gz → 0.3.3__tar.gz
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.
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/PKG-INFO +2 -2
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/pyproject.toml +1 -1
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/axon.py +2 -4
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/cache.py +1 -1
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/export.py +11 -11
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/helpers.py +6 -8
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/navigation.py +2 -2
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/query.py +7 -9
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/remote.py +6 -6
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/run.py +7 -10
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/show.py +3 -3
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/workspace.py +2 -2
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/main.py +2 -3
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/tui.py +10 -10
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/ui_utils.py +2 -4
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/view_model.py +11 -12
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/integration/test_navigation_commands.py +2 -2
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/integration/test_workspace_commands.py +3 -2
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/unit/test_view_model.py +13 -14
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/.gitignore +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/__init__.py +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/__init__.py +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/cache.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/export.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/helpers.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/query.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/show.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/commands/ui.py +1 -1
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/config.py +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/config.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/logger_config.py +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/logger_config.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/rendering.py +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/rendering.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/tui.css +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/tui.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/ui_utils.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/src/quipu/cli/view_model.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/stitcher.lock +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/integration/conftest.py +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/integration/test_cache_commands.py +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/integration/test_cache_commands.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/integration/test_cli_interaction.py +1 -1
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/integration/test_cli_interaction.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/integration/test_export_command.py +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/integration/test_export_command.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/integration/test_query_commands.py +1 -1
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/integration/test_query_commands.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/integration/test_unfriendly_paths.py +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/integration/test_unfriendly_paths.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/unit/test_tui_logic.py +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/unit/test_tui_logic.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/unit/test_tui_reachability.py +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/unit/test_tui_reachability.stitcher.yaml +0 -0
- {pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/unit/test_view_model.stitcher.yaml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: pyquipu-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: The command-line interface for Quipu.
|
|
5
5
|
Project-URL: Homepage, https://github.com/quipu-dev/Quipu
|
|
6
6
|
Project-URL: Repository, https://github.com/quipu-dev/Quipu
|
|
@@ -2,7 +2,7 @@ import inspect
|
|
|
2
2
|
import logging
|
|
3
3
|
import sys
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Annotated
|
|
5
|
+
from typing import Annotated
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
from needle.pointer import L
|
|
@@ -19,9 +19,7 @@ def register(app: typer.Typer):
|
|
|
19
19
|
@app.command(name="axon", help="无状态执行 Plan 文件,绕过 Quipu 引擎。")
|
|
20
20
|
def axon_command(
|
|
21
21
|
ctx: typer.Context,
|
|
22
|
-
file: Annotated[
|
|
23
|
-
Optional[Path], typer.Argument(help="包含 Markdown 指令的文件路径。", resolve_path=True)
|
|
24
|
-
] = None,
|
|
22
|
+
file: Annotated[Path | None, typer.Argument(help="包含 Markdown 指令的文件路径。", resolve_path=True)] = None,
|
|
25
23
|
work_dir: Annotated[
|
|
26
24
|
Path,
|
|
27
25
|
typer.Option(
|
|
@@ -2,7 +2,7 @@ import logging
|
|
|
2
2
|
import re
|
|
3
3
|
import shutil
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Annotated
|
|
5
|
+
from typing import Annotated
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
import yaml
|
|
@@ -49,9 +49,9 @@ def _format_frontmatter(node: QuipuNode) -> str:
|
|
|
49
49
|
|
|
50
50
|
def _generate_navbar(
|
|
51
51
|
current_node: QuipuNode,
|
|
52
|
-
exported_hashes_set:
|
|
53
|
-
filename_map:
|
|
54
|
-
hidden_link_types:
|
|
52
|
+
exported_hashes_set: set[str],
|
|
53
|
+
filename_map: dict[str, str],
|
|
54
|
+
hidden_link_types: set[str],
|
|
55
55
|
) -> str:
|
|
56
56
|
nav_links = []
|
|
57
57
|
|
|
@@ -102,9 +102,9 @@ def _generate_file_content(
|
|
|
102
102
|
engine: Engine,
|
|
103
103
|
no_frontmatter: bool,
|
|
104
104
|
no_nav: bool,
|
|
105
|
-
exported_hashes_set:
|
|
106
|
-
filename_map:
|
|
107
|
-
hidden_link_types:
|
|
105
|
+
exported_hashes_set: set[str],
|
|
106
|
+
filename_map: dict[str, str],
|
|
107
|
+
hidden_link_types: set[str],
|
|
108
108
|
) -> str:
|
|
109
109
|
parts = []
|
|
110
110
|
if not no_frontmatter:
|
|
@@ -138,14 +138,14 @@ def register(app: typer.Typer):
|
|
|
138
138
|
output_dir: Annotated[Path, typer.Option("--output-dir", "-o", help="导出目录", resolve_path=True)] = Path(
|
|
139
139
|
"./.quipu/export"
|
|
140
140
|
),
|
|
141
|
-
limit: Annotated[
|
|
142
|
-
since: Annotated[
|
|
143
|
-
until: Annotated[
|
|
141
|
+
limit: Annotated[int | None, typer.Option("--limit", "-n", help="限制最新节点数量")] = None,
|
|
142
|
+
since: Annotated[str | None, typer.Option("--since", help="起始时间戳 (YYYY-MM-DD HH:MM)")] = None,
|
|
143
|
+
until: Annotated[str | None, typer.Option("--until", help="截止时间戳 (YYYY-MM-DD HH:MM)")] = None,
|
|
144
144
|
zip_output: Annotated[bool, typer.Option("--zip", help="压缩导出目录")] = False,
|
|
145
145
|
no_nav: Annotated[bool, typer.Option("--no-nav", help="禁用导航栏")] = False,
|
|
146
146
|
no_frontmatter: Annotated[bool, typer.Option("--no-frontmatter", help="禁用 Frontmatter")] = False,
|
|
147
147
|
hide_link_type: Annotated[
|
|
148
|
-
|
|
148
|
+
list[str] | None,
|
|
149
149
|
typer.Option(
|
|
150
150
|
"--hide-link-type", help="禁用特定类型的导航链接 (可多次使用: summary, branch, parent, child)"
|
|
151
151
|
),
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from collections.abc import Generator
|
|
2
3
|
from contextlib import contextmanager
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import Dict, Generator, List, Optional
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
|
-
from quipu.application.factory import create_engine
|
|
9
8
|
from needle.pointer import L
|
|
9
|
+
from quipu.application.factory import create_engine
|
|
10
10
|
from quipu.common.bus import bus
|
|
11
11
|
from quipu.engine.state_machine import Engine
|
|
12
12
|
from quipu.spec.models.graph import QuipuNode
|
|
@@ -28,7 +28,7 @@ def engine_context(work_dir: Path) -> Generator[Engine, None, None]:
|
|
|
28
28
|
engine.close()
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def _find_current_node(engine: Engine, graph:
|
|
31
|
+
def _find_current_node(engine: Engine, graph: dict[str, QuipuNode]) -> QuipuNode | None:
|
|
32
32
|
current_hash = engine.git_db.get_tree_hash()
|
|
33
33
|
# 修复:直接从 graph 中通过 output_tree hash 查找
|
|
34
34
|
for node in graph.values():
|
|
@@ -46,14 +46,12 @@ def _execute_visit(ctx: typer.Context, engine: Engine, target_hash: str, msg_id:
|
|
|
46
46
|
engine.visit(target_hash)
|
|
47
47
|
bus.success(L.navigation.success.visit, short_hash=target_hash[:7])
|
|
48
48
|
except Exception as e:
|
|
49
|
-
logger.
|
|
49
|
+
logger.exception(f"导航操作失败 (目标哈希: {target_hash[:12]})")
|
|
50
50
|
bus.error(L.navigation.error.generic, error=str(e))
|
|
51
51
|
ctx.exit(1)
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
def filter_nodes(
|
|
55
|
-
nodes: List[QuipuNode], limit: Optional[int], since: Optional[str], until: Optional[str]
|
|
56
|
-
) -> List[QuipuNode]:
|
|
54
|
+
def filter_nodes(nodes: list[QuipuNode], limit: int | None, since: str | None, until: str | None) -> list[QuipuNode]:
|
|
57
55
|
filtered = nodes
|
|
58
56
|
if since:
|
|
59
57
|
try:
|
|
@@ -72,7 +70,7 @@ def filter_nodes(
|
|
|
72
70
|
return filtered
|
|
73
71
|
|
|
74
72
|
|
|
75
|
-
def filter_reachable_nodes(engine: Engine, nodes:
|
|
73
|
+
def filter_reachable_nodes(engine: Engine, nodes: list[QuipuNode]) -> list[QuipuNode]:
|
|
76
74
|
current_node = _find_current_node(engine, engine.history_graph)
|
|
77
75
|
if not current_node:
|
|
78
76
|
# 如果工作区是脏的,无法确定起点,返回所有节点
|
|
@@ -218,7 +218,7 @@ def register(app: typer.Typer):
|
|
|
218
218
|
else:
|
|
219
219
|
bus.warning(L.navigation.back.atStart)
|
|
220
220
|
except Exception as e:
|
|
221
|
-
logger.
|
|
221
|
+
logger.exception("后退操作失败")
|
|
222
222
|
bus.error(L.navigation.back.error, error=str(e))
|
|
223
223
|
ctx.exit(1)
|
|
224
224
|
|
|
@@ -235,6 +235,6 @@ def register(app: typer.Typer):
|
|
|
235
235
|
else:
|
|
236
236
|
bus.warning(L.navigation.forward.atEnd)
|
|
237
237
|
except Exception as e:
|
|
238
|
-
logger.
|
|
238
|
+
logger.exception("前进操作失败")
|
|
239
239
|
bus.error(L.navigation.forward.error, error=str(e))
|
|
240
240
|
ctx.exit(1)
|
|
@@ -2,7 +2,7 @@ import dataclasses
|
|
|
2
2
|
import json
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Annotated
|
|
5
|
+
from typing import Annotated
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
from needle.pointer import L
|
|
@@ -13,7 +13,7 @@ from ..config import DEFAULT_WORK_DIR
|
|
|
13
13
|
from .helpers import engine_context, filter_nodes, filter_reachable_nodes
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def _nodes_to_json_str(nodes:
|
|
16
|
+
def _nodes_to_json_str(nodes: list[QuipuNode]) -> str:
|
|
17
17
|
EXCLUDED_FIELDS = {"parent", "children", "content", "filename"}
|
|
18
18
|
node_list = []
|
|
19
19
|
for node in nodes:
|
|
@@ -44,9 +44,9 @@ def register(app: typer.Typer):
|
|
|
44
44
|
"--work-dir", "-w", help="操作执行的根目录(工作区)", file_okay=False, dir_okay=True, resolve_path=True
|
|
45
45
|
),
|
|
46
46
|
] = DEFAULT_WORK_DIR,
|
|
47
|
-
limit: Annotated[
|
|
48
|
-
since: Annotated[
|
|
49
|
-
until: Annotated[
|
|
47
|
+
limit: Annotated[int | None, typer.Option("--limit", "-n", help="限制显示的节点数量。")] = None,
|
|
48
|
+
since: Annotated[str | None, typer.Option("--since", help="起始时间戳 (YYYY-MM-DD HH:MM)。")] = None,
|
|
49
|
+
until: Annotated[str | None, typer.Option("--until", help="截止时间戳 (YYYY-MM-DD HH:MM)。")] = None,
|
|
50
50
|
reachable_only: Annotated[
|
|
51
51
|
bool, typer.Option("--reachable-only", help="仅显示与当前工作区状态直接相关的节点。")
|
|
52
52
|
] = False,
|
|
@@ -98,11 +98,9 @@ def register(app: typer.Typer):
|
|
|
98
98
|
def find_command(
|
|
99
99
|
ctx: typer.Context,
|
|
100
100
|
summary_regex: Annotated[
|
|
101
|
-
|
|
102
|
-
] = None,
|
|
103
|
-
node_type: Annotated[
|
|
104
|
-
Optional[str], typer.Option("--type", "-t", help="节点类型 ('plan' 或 'capture')。")
|
|
101
|
+
str | None, typer.Option("--summary", "-s", help="用于匹配节点摘要的正则表达式 (不区分大小写)。")
|
|
105
102
|
] = None,
|
|
103
|
+
node_type: Annotated[str | None, typer.Option("--type", "-t", help="节点类型 ('plan' 或 'capture')。")] = None,
|
|
106
104
|
limit: Annotated[int, typer.Option("--limit", "-n", help="返回的最大结果数量。")] = 10,
|
|
107
105
|
work_dir: Annotated[Path, typer.Option("--work-dir", "-w", help="工作区根目录。")] = DEFAULT_WORK_DIR,
|
|
108
106
|
json_output: Annotated[bool, typer.Option("--json", help="以 JSON 格式输出结果。")] = False,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import subprocess
|
|
2
2
|
from enum import Enum
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Annotated
|
|
4
|
+
from typing import Annotated
|
|
5
5
|
|
|
6
6
|
import typer
|
|
7
|
-
from quipu.application.utils import find_git_repository_root
|
|
8
7
|
from needle.pointer import L
|
|
8
|
+
from quipu.application.utils import find_git_repository_root
|
|
9
9
|
from quipu.common.bus import bus
|
|
10
10
|
from quipu.common.identity import get_user_id_from_email
|
|
11
11
|
from quipu.engine.config import ConfigManager
|
|
@@ -35,7 +35,7 @@ def register(app: typer.Typer):
|
|
|
35
35
|
),
|
|
36
36
|
] = DEFAULT_WORK_DIR,
|
|
37
37
|
remote_option: Annotated[
|
|
38
|
-
|
|
38
|
+
str | None, typer.Option("--remote", "-r", help="Git 远程仓库的名称 (覆盖配置文件)。")
|
|
39
39
|
] = None,
|
|
40
40
|
mode: Annotated[
|
|
41
41
|
SyncMode,
|
|
@@ -85,7 +85,7 @@ def register(app: typer.Typer):
|
|
|
85
85
|
match mode:
|
|
86
86
|
case SyncMode.BIDIRECTIONAL:
|
|
87
87
|
bus.info(L.sync.run.info.pulling)
|
|
88
|
-
for target_id in sorted(
|
|
88
|
+
for target_id in sorted(target_ids_to_fetch):
|
|
89
89
|
git_db.fetch_quipu_refs(remote, target_id)
|
|
90
90
|
bus.info(L.sync.run.info.reconciling)
|
|
91
91
|
git_db.reconcile_local_with_remote(remote, final_user_id)
|
|
@@ -95,7 +95,7 @@ def register(app: typer.Typer):
|
|
|
95
95
|
|
|
96
96
|
case SyncMode.PULL_ONLY:
|
|
97
97
|
bus.info(L.sync.run.info.pulling)
|
|
98
|
-
for target_id in sorted(
|
|
98
|
+
for target_id in sorted(target_ids_to_fetch):
|
|
99
99
|
git_db.fetch_quipu_refs(remote, target_id)
|
|
100
100
|
bus.info(L.sync.run.info.reconciling)
|
|
101
101
|
git_db.reconcile_local_with_remote(remote, final_user_id)
|
|
@@ -103,7 +103,7 @@ def register(app: typer.Typer):
|
|
|
103
103
|
|
|
104
104
|
case SyncMode.PULL_PRUNE:
|
|
105
105
|
bus.info(L.sync.run.info.pullingPrune)
|
|
106
|
-
for target_id in sorted(
|
|
106
|
+
for target_id in sorted(target_ids_to_fetch):
|
|
107
107
|
git_db.fetch_quipu_refs(remote, target_id)
|
|
108
108
|
bus.info(L.sync.run.info.reconciling)
|
|
109
109
|
git_db.reconcile_local_with_remote(remote, final_user_id)
|
|
@@ -2,11 +2,11 @@ import inspect
|
|
|
2
2
|
import logging
|
|
3
3
|
import sys
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Annotated
|
|
5
|
+
from typing import Annotated
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
|
-
from quipu.application.controller import run_quipu
|
|
9
8
|
from needle.pointer import L
|
|
9
|
+
from quipu.application.controller import run_quipu
|
|
10
10
|
from quipu.common.bus import bus
|
|
11
11
|
|
|
12
12
|
from ..config import DEFAULT_ENTRY_FILE, DEFAULT_WORK_DIR
|
|
@@ -20,9 +20,7 @@ def register(app: typer.Typer):
|
|
|
20
20
|
@app.command(name="run", help="执行 Plan 文件并记录到 Quipu 历史。")
|
|
21
21
|
def run_command(
|
|
22
22
|
ctx: typer.Context,
|
|
23
|
-
file: Annotated[
|
|
24
|
-
Optional[Path], typer.Argument(help="包含 Markdown 指令的文件路径。", resolve_path=True)
|
|
25
|
-
] = None,
|
|
23
|
+
file: Annotated[Path | None, typer.Argument(help="包含 Markdown 指令的文件路径。", resolve_path=True)] = None,
|
|
26
24
|
work_dir: Annotated[
|
|
27
25
|
Path,
|
|
28
26
|
typer.Option(
|
|
@@ -75,11 +73,10 @@ def register(app: typer.Typer):
|
|
|
75
73
|
bus.error(L.common.error.fileNotFound, path=file)
|
|
76
74
|
bus.warning(L.run.error.ambiguousCommand, command=file.name)
|
|
77
75
|
ctx.exit(1)
|
|
78
|
-
if not content.strip():
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
ctx.exit(0)
|
|
76
|
+
if not content.strip() and not file:
|
|
77
|
+
bus.warning(L.run.warning.noInput, filename=DEFAULT_ENTRY_FILE.name)
|
|
78
|
+
bus.info(L.run.info.usageHint)
|
|
79
|
+
ctx.exit(0)
|
|
83
80
|
|
|
84
81
|
logger.info(f"已加载指令源: {source_desc}")
|
|
85
82
|
logger.info(f"工作区根目录: {work_dir}")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Annotated
|
|
4
|
+
from typing import Annotated
|
|
5
5
|
|
|
6
6
|
import typer
|
|
7
7
|
from needle.pointer import L
|
|
@@ -15,7 +15,7 @@ from .helpers import engine_context
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def _find_target_node(graph:
|
|
18
|
+
def _find_target_node(graph: dict, hash_prefix: str):
|
|
19
19
|
matches = [
|
|
20
20
|
node
|
|
21
21
|
for node in graph.values()
|
|
@@ -48,7 +48,7 @@ def register(app: typer.Typer):
|
|
|
48
48
|
] = DEFAULT_WORK_DIR,
|
|
49
49
|
json_output: Annotated[bool, typer.Option("--json", help="以 JSON 格式将结果输出到 stdout。")] = False,
|
|
50
50
|
extract: Annotated[
|
|
51
|
-
|
|
51
|
+
list[str] | None, typer.Option("--extract", "-e", help="仅提取并显示指定文件的内容 (可多次使用)。")
|
|
52
52
|
] = None,
|
|
53
53
|
):
|
|
54
54
|
with engine_context(work_dir) as engine:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
from typing import Annotated
|
|
2
|
+
from typing import Annotated
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
5
|
from needle.pointer import L
|
|
@@ -15,7 +15,7 @@ def register(app: typer.Typer):
|
|
|
15
15
|
@app.command(help="将当前工作区的变更创建为一个新的快照节点。")
|
|
16
16
|
def save(
|
|
17
17
|
ctx: typer.Context,
|
|
18
|
-
message: Annotated[
|
|
18
|
+
message: Annotated[str | None, typer.Argument(help="本次快照的简短描述。")] = None,
|
|
19
19
|
work_dir: Annotated[
|
|
20
20
|
Path,
|
|
21
21
|
typer.Option(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import importlib.metadata
|
|
2
2
|
import logging
|
|
3
|
-
from typing import Annotated
|
|
3
|
+
from typing import Annotated
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
6
|
from quipu.common.bus import bus
|
|
@@ -54,14 +54,13 @@ app.add_typer(cache.cache_app)
|
|
|
54
54
|
@app.callback()
|
|
55
55
|
def main(
|
|
56
56
|
version: Annotated[
|
|
57
|
-
|
|
57
|
+
bool | None,
|
|
58
58
|
typer.Option("--version", "-V", callback=version_callback, is_eager=True, help="显示版本信息并退出。"),
|
|
59
59
|
] = None,
|
|
60
60
|
):
|
|
61
61
|
"""
|
|
62
62
|
Quipu: 一个基于 Git 的、用于文件系统状态溯源与文学化操作的工具。
|
|
63
63
|
"""
|
|
64
|
-
pass
|
|
65
64
|
|
|
66
65
|
|
|
67
66
|
# 注册顶级命令
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from enum import Enum, auto
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import ClassVar
|
|
5
5
|
|
|
6
6
|
from quipu.application.factory import create_engine
|
|
7
7
|
from quipu.engine.state_machine import Engine
|
|
@@ -29,11 +29,11 @@ class ContentViewSate(Enum):
|
|
|
29
29
|
SHOWING_CONTENT = auto()
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
class QuipuUiApp(App[
|
|
32
|
+
class QuipuUiApp(App[UiResult | None]):
|
|
33
33
|
CSS_PATH = "tui.css"
|
|
34
34
|
TITLE = "Quipu History Explorer"
|
|
35
35
|
|
|
36
|
-
BINDINGS = [
|
|
36
|
+
BINDINGS: ClassVar[list[Binding]] = [
|
|
37
37
|
Binding("q", "quit", "退出"),
|
|
38
38
|
Binding("space", "checkout_node", "检出节点"),
|
|
39
39
|
Binding("enter", "checkout_node", "检出节点"),
|
|
@@ -54,12 +54,12 @@ class QuipuUiApp(App[Optional[UiResult]]):
|
|
|
54
54
|
def __init__(self, work_dir: Path, initial_raw_mode: bool = False):
|
|
55
55
|
super().__init__()
|
|
56
56
|
self.work_dir = work_dir
|
|
57
|
-
self.engine:
|
|
58
|
-
self.view_model:
|
|
57
|
+
self.engine: Engine | None = None
|
|
58
|
+
self.view_model: GraphViewModel | None = None
|
|
59
59
|
|
|
60
60
|
# --- State Machine ---
|
|
61
61
|
self.content_view_state = ContentViewSate.HIDDEN
|
|
62
|
-
self.update_timer:
|
|
62
|
+
self.update_timer: Timer | None = None
|
|
63
63
|
self.debounce_delay_seconds: float = 0.50
|
|
64
64
|
self.markdown_enabled = not initial_raw_mode
|
|
65
65
|
|
|
@@ -177,10 +177,10 @@ class QuipuUiApp(App[Optional[UiResult]]):
|
|
|
177
177
|
self._focus_current_node(table)
|
|
178
178
|
self._update_header()
|
|
179
179
|
|
|
180
|
-
def _populate_table(self, table: DataTable, nodes:
|
|
180
|
+
def _populate_table(self, table: DataTable, nodes: list[QuipuNode]):
|
|
181
181
|
assert self.view_model is not None
|
|
182
182
|
# 移除了过滤逻辑,因为 ViewModel 已经处理
|
|
183
|
-
tracks: list[
|
|
183
|
+
tracks: list[str | None] = []
|
|
184
184
|
|
|
185
185
|
for node in nodes:
|
|
186
186
|
is_reachable = self.view_model.is_reachable(node.output_tree)
|
|
@@ -279,8 +279,8 @@ class QuipuUiApp(App[Optional[UiResult]]):
|
|
|
279
279
|
# LookupError 捕获 RowKeyError 等
|
|
280
280
|
logger.warning(f"DEBUG: Row key {row_key} not found in DataTable.")
|
|
281
281
|
|
|
282
|
-
except Exception
|
|
283
|
-
logger.
|
|
282
|
+
except Exception:
|
|
283
|
+
logger.exception("DEBUG: Failed to focus current node")
|
|
284
284
|
|
|
285
285
|
def _update_loading_preview(self):
|
|
286
286
|
assert self.view_model is not None
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from typing import List, Optional
|
|
2
|
-
|
|
3
1
|
import click
|
|
4
2
|
import typer
|
|
5
3
|
from needle.pointer import L
|
|
@@ -7,7 +5,7 @@ from quipu.common.bus import bus
|
|
|
7
5
|
from quipu.spec.exceptions import OperationCancelledError
|
|
8
6
|
|
|
9
7
|
|
|
10
|
-
def confirmation_handler_for_executor(diff_lines:
|
|
8
|
+
def confirmation_handler_for_executor(diff_lines: list[str], prompt: str) -> bool:
|
|
11
9
|
# 原始逻辑是 `char.lower() != "n"`,这相当于默认为 True
|
|
12
10
|
confirmed = prompt_for_confirmation(prompt=prompt, diff_lines=diff_lines, default=True)
|
|
13
11
|
if not confirmed:
|
|
@@ -17,7 +15,7 @@ def confirmation_handler_for_executor(diff_lines: List[str], prompt: str) -> boo
|
|
|
17
15
|
return True
|
|
18
16
|
|
|
19
17
|
|
|
20
|
-
def prompt_for_confirmation(prompt: str, diff_lines:
|
|
18
|
+
def prompt_for_confirmation(prompt: str, diff_lines: list[str] | None = None, default: bool = False) -> bool:
|
|
21
19
|
if diff_lines:
|
|
22
20
|
bus.info(L.prompt.ui.diffHeader)
|
|
23
21
|
for line in diff_lines:
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import math
|
|
2
|
-
from typing import Dict, List, Optional, Set
|
|
3
2
|
|
|
4
3
|
from quipu.spec.models.graph import QuipuNode
|
|
5
4
|
from quipu.spec.protocols.storage import HistoryReader
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
class GraphViewModel:
|
|
9
|
-
def __init__(self, reader: HistoryReader, current_output_tree_hash:
|
|
8
|
+
def __init__(self, reader: HistoryReader, current_output_tree_hash: str | None, page_size: int = 50):
|
|
10
9
|
self.reader = reader
|
|
11
10
|
self.current_output_tree_hash = current_output_tree_hash
|
|
12
11
|
self.page_size = page_size
|
|
@@ -18,10 +17,10 @@ class GraphViewModel:
|
|
|
18
17
|
|
|
19
18
|
# --- TUI 交互状态 ---
|
|
20
19
|
self.show_unreachable: bool = True
|
|
21
|
-
self.current_page_nodes:
|
|
22
|
-
self.current_selected_node:
|
|
23
|
-
self._node_by_key:
|
|
24
|
-
self.reachable_set:
|
|
20
|
+
self.current_page_nodes: list[QuipuNode] = []
|
|
21
|
+
self.current_selected_node: QuipuNode | None = None
|
|
22
|
+
self._node_by_key: dict[str, QuipuNode] = {}
|
|
23
|
+
self.reachable_set: set[str] = set()
|
|
25
24
|
|
|
26
25
|
def initialize(self):
|
|
27
26
|
self.total_nodes = self.reader.get_node_count()
|
|
@@ -58,7 +57,7 @@ class GraphViewModel:
|
|
|
58
57
|
# e.g. pos 0 -> page 1; pos 49 -> page 1; pos 50 -> page 2
|
|
59
58
|
return (position // self.page_size) + 1
|
|
60
59
|
|
|
61
|
-
def load_page(self, page_number: int) ->
|
|
60
|
+
def load_page(self, page_number: int) -> list[QuipuNode]:
|
|
62
61
|
if not (1 <= page_number <= self.total_pages):
|
|
63
62
|
self.current_page_nodes = []
|
|
64
63
|
self._node_by_key = {}
|
|
@@ -74,26 +73,26 @@ class GraphViewModel:
|
|
|
74
73
|
def toggle_unreachable(self):
|
|
75
74
|
self.show_unreachable = not self.show_unreachable
|
|
76
75
|
|
|
77
|
-
def get_nodes_to_render(self) ->
|
|
76
|
+
def get_nodes_to_render(self) -> list[QuipuNode]:
|
|
78
77
|
if self.show_unreachable:
|
|
79
78
|
return self.current_page_nodes
|
|
80
79
|
return [node for node in self.current_page_nodes if self.is_reachable(node.output_tree)]
|
|
81
80
|
|
|
82
|
-
def select_node_by_key(self, key: str) ->
|
|
81
|
+
def select_node_by_key(self, key: str) -> QuipuNode | None:
|
|
83
82
|
node = self._node_by_key.get(key)
|
|
84
83
|
self.current_selected_node = node
|
|
85
84
|
return node
|
|
86
85
|
|
|
87
|
-
def get_selected_node(self) ->
|
|
86
|
+
def get_selected_node(self) -> QuipuNode | None:
|
|
88
87
|
return self.current_selected_node
|
|
89
88
|
|
|
90
89
|
def get_public_content(self, node: QuipuNode) -> str:
|
|
91
90
|
return self.reader.get_node_content(node) or ""
|
|
92
91
|
|
|
93
|
-
def previous_page(self) ->
|
|
92
|
+
def previous_page(self) -> list[QuipuNode]:
|
|
94
93
|
return self.load_page(self.current_page - 1)
|
|
95
94
|
|
|
96
|
-
def next_page(self) ->
|
|
95
|
+
def next_page(self) -> list[QuipuNode]:
|
|
97
96
|
return self.load_page(self.current_page + 1)
|
|
98
97
|
|
|
99
98
|
def get_content_bundle(self, node: QuipuNode) -> str:
|
|
@@ -14,7 +14,7 @@ def populated_workspace(quipu_workspace):
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def test_cli_back_forward_flow(runner, populated_workspace, monkeypatch):
|
|
17
|
-
workspace, hash_a,
|
|
17
|
+
workspace, hash_a, _hash_b = populated_workspace
|
|
18
18
|
mock_bus_nav = MagicMock()
|
|
19
19
|
mock_bus_helper = MagicMock()
|
|
20
20
|
monkeypatch.setattr("quipu.cli.commands.navigation.bus", mock_bus_nav)
|
|
@@ -41,7 +41,7 @@ def test_cli_back_forward_flow(runner, populated_workspace, monkeypatch):
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def test_cli_boundary_messages(runner, populated_workspace, monkeypatch):
|
|
44
|
-
workspace, hash_a,
|
|
44
|
+
workspace, hash_a, _hash_b = populated_workspace
|
|
45
45
|
mock_bus = MagicMock()
|
|
46
46
|
monkeypatch.setattr("quipu.cli.commands.navigation.bus", mock_bus)
|
|
47
47
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from needle.pointer import L
|
|
2
1
|
from unittest import mock
|
|
3
2
|
from unittest.mock import MagicMock
|
|
4
3
|
|
|
4
|
+
from needle.pointer import L
|
|
5
5
|
from quipu.cli.main import app
|
|
6
6
|
|
|
7
7
|
|
|
@@ -49,12 +49,13 @@ def test_discard_interactive_abort(runner, quipu_workspace, monkeypatch):
|
|
|
49
49
|
work_dir, _, engine = quipu_workspace
|
|
50
50
|
mock_bus = MagicMock()
|
|
51
51
|
monkeypatch.setattr("quipu.cli.commands.workspace.bus", mock_bus)
|
|
52
|
+
monkeypatch.setattr("click.getchar", lambda echo=False: "n")
|
|
52
53
|
|
|
53
54
|
(work_dir / "file.txt").write_text("v1")
|
|
54
55
|
engine.capture_drift(engine.git_db.get_tree_hash())
|
|
55
56
|
(work_dir / "file.txt").write_text("v2")
|
|
56
57
|
|
|
57
|
-
result = runner.invoke(app, ["discard", "-w", str(work_dir)]
|
|
58
|
+
result = runner.invoke(app, ["discard", "-w", str(work_dir)])
|
|
58
59
|
|
|
59
60
|
assert result.exit_code == 1
|
|
60
61
|
mock_bus.warning.assert_called_once_with(L.common.prompt.cancel)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Dict, List, Optional, Set
|
|
4
3
|
|
|
5
4
|
import pytest
|
|
6
5
|
from quipu.cli.view_model import GraphViewModel
|
|
@@ -11,10 +10,10 @@ from quipu.spec.protocols.storage import HistoryReader
|
|
|
11
10
|
class MockHistoryReader(HistoryReader):
|
|
12
11
|
def __init__(
|
|
13
12
|
self,
|
|
14
|
-
nodes:
|
|
15
|
-
ancestors:
|
|
16
|
-
descendants:
|
|
17
|
-
private_data:
|
|
13
|
+
nodes: list[QuipuNode],
|
|
14
|
+
ancestors: set[str] | None = None,
|
|
15
|
+
descendants: set[str] | None = None,
|
|
16
|
+
private_data: dict[str, str] | None = None,
|
|
18
17
|
):
|
|
19
18
|
self._nodes = sorted(nodes, key=lambda n: n.timestamp, reverse=True)
|
|
20
19
|
self._ancestors = ancestors or set()
|
|
@@ -24,13 +23,13 @@ class MockHistoryReader(HistoryReader):
|
|
|
24
23
|
def get_node_count(self) -> int:
|
|
25
24
|
return len(self._nodes)
|
|
26
25
|
|
|
27
|
-
def load_nodes_paginated(self, limit: int, offset: int) ->
|
|
26
|
+
def load_nodes_paginated(self, limit: int, offset: int) -> list[QuipuNode]:
|
|
28
27
|
return self._nodes[offset : offset + limit]
|
|
29
28
|
|
|
30
|
-
def get_ancestor_output_trees(self, start_output_tree_hash: str) ->
|
|
29
|
+
def get_ancestor_output_trees(self, start_output_tree_hash: str) -> set[str]:
|
|
31
30
|
return self._ancestors
|
|
32
31
|
|
|
33
|
-
def get_descendant_output_trees(self, start_output_tree_hash: str) ->
|
|
32
|
+
def get_descendant_output_trees(self, start_output_tree_hash: str) -> set[str]:
|
|
34
33
|
return self._descendants
|
|
35
34
|
|
|
36
35
|
def get_node_position(self, output_tree_hash: str) -> int:
|
|
@@ -39,26 +38,26 @@ class MockHistoryReader(HistoryReader):
|
|
|
39
38
|
return i
|
|
40
39
|
return -1
|
|
41
40
|
|
|
42
|
-
def get_private_data(self, node_commit_hash: str) ->
|
|
41
|
+
def get_private_data(self, node_commit_hash: str) -> str | None:
|
|
43
42
|
return self._private_data.get(node_commit_hash)
|
|
44
43
|
|
|
45
44
|
def get_node_content(self, node: QuipuNode) -> str:
|
|
46
45
|
# For simplicity, mock content is stored in the node's summary
|
|
47
46
|
return node.summary
|
|
48
47
|
|
|
49
|
-
def get_node_blobs(self, commit_hash: str) ->
|
|
48
|
+
def get_node_blobs(self, commit_hash: str) -> dict[str, bytes]:
|
|
50
49
|
return {}
|
|
51
50
|
|
|
52
51
|
# --- Unused abstract methods ---
|
|
53
|
-
def load_all_nodes(self) ->
|
|
52
|
+
def load_all_nodes(self) -> list[QuipuNode]:
|
|
54
53
|
return self._nodes
|
|
55
54
|
|
|
56
55
|
def find_nodes(
|
|
57
56
|
self,
|
|
58
|
-
summary_regex:
|
|
59
|
-
node_type:
|
|
57
|
+
summary_regex: str | None = None,
|
|
58
|
+
node_type: str | None = None,
|
|
60
59
|
limit: int = 10,
|
|
61
|
-
) ->
|
|
60
|
+
) -> list[QuipuNode]:
|
|
62
61
|
return []
|
|
63
62
|
|
|
64
63
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -3,8 +3,8 @@ from pathlib import Path
|
|
|
3
3
|
from typing import Annotated
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
|
-
from quipu.application.factory import create_engine
|
|
7
6
|
from needle.pointer import L
|
|
7
|
+
from quipu.application.factory import create_engine
|
|
8
8
|
from quipu.common.bus import bus
|
|
9
9
|
|
|
10
10
|
from ..config import DEFAULT_WORK_DIR, LOG_LEVEL
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/integration/test_cli_interaction.stitcher.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyquipu_cli-0.3.2 → pyquipu_cli-0.3.3}/tests/integration/test_unfriendly_paths.stitcher.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|