pyquipu-cli 0.2.0__tar.gz → 0.3.0__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.
Files changed (57) hide show
  1. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/PKG-INFO +7 -7
  2. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/pyproject.toml +7 -8
  3. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/axon.py +6 -5
  4. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/cache.py +11 -10
  5. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/export.py +11 -10
  6. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/helpers.py +6 -5
  7. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/navigation.py +37 -26
  8. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/query.py +9 -8
  9. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/remote.py +24 -23
  10. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/run.py +11 -10
  11. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/show.py +16 -10
  12. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/ui.py +6 -5
  13. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/workspace.py +13 -11
  14. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/main.py +1 -1
  15. pyquipu_cli-0.3.0/src/quipu/cli/rendering.py +28 -0
  16. pyquipu_cli-0.3.0/src/quipu/cli/rendering.stitcher.yaml +6 -0
  17. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/tui.py +14 -0
  18. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/ui_utils.py +7 -4
  19. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/stitcher.lock +14 -4
  20. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_cache_commands.py +13 -12
  21. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_cli_interaction.py +5 -5
  22. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_export_command.py +7 -6
  23. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_navigation_commands.py +24 -3
  24. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_query_commands.py +5 -4
  25. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_unfriendly_paths.py +5 -4
  26. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_workspace_commands.py +5 -4
  27. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/unit/test_tui_reachability.py +7 -7
  28. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/unit/test_view_model.py +3 -3
  29. pyquipu_cli-0.2.0/src/quipu/cli/rendering.py +0 -18
  30. pyquipu_cli-0.2.0/src/quipu/cli/rendering.stitcher.yaml +0 -2
  31. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/.gitignore +0 -0
  32. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/__init__.py +0 -0
  33. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/__init__.py +0 -0
  34. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/cache.stitcher.yaml +0 -0
  35. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/export.stitcher.yaml +0 -0
  36. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/helpers.stitcher.yaml +0 -0
  37. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/query.stitcher.yaml +0 -0
  38. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/show.stitcher.yaml +0 -0
  39. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/config.py +0 -0
  40. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/config.stitcher.yaml +0 -0
  41. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/logger_config.py +0 -0
  42. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/logger_config.stitcher.yaml +0 -0
  43. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/tui.css +0 -0
  44. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/tui.stitcher.yaml +0 -0
  45. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/ui_utils.stitcher.yaml +0 -0
  46. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/view_model.py +0 -0
  47. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/view_model.stitcher.yaml +0 -0
  48. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/conftest.py +0 -0
  49. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_cache_commands.stitcher.yaml +0 -0
  50. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_cli_interaction.stitcher.yaml +0 -0
  51. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_export_command.stitcher.yaml +0 -0
  52. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_query_commands.stitcher.yaml +0 -0
  53. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_unfriendly_paths.stitcher.yaml +0 -0
  54. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/unit/test_tui_logic.py +0 -0
  55. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/unit/test_tui_logic.stitcher.yaml +0 -0
  56. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/unit/test_tui_reachability.stitcher.yaml +0 -0
  57. {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/unit/test_view_model.stitcher.yaml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyquipu-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
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
@@ -20,11 +20,11 @@ Classifier: Topic :: Software Development :: Version Control
20
20
  Classifier: Topic :: Text Processing
21
21
  Classifier: Typing :: Typed
22
22
  Requires-Python: >=3.10
23
- Requires-Dist: pyquipu-application~=0.2.0
24
- Requires-Dist: pyquipu-bus~=0.2.0
25
- Requires-Dist: pyquipu-common~=0.1.1
26
- Requires-Dist: pyquipu-engine~=0.2.0
27
- Requires-Dist: pyquipu-runtime~=0.2.0
28
- Requires-Dist: pyquipu-spec~=0.1.0
23
+ Requires-Dist: click>=8.1.0
24
+ Requires-Dist: pyquipu-application~=0.3.0
25
+ Requires-Dist: pyquipu-common~=0.2.0
26
+ Requires-Dist: pyquipu-engine~=0.3.0
27
+ Requires-Dist: pyquipu-runtime~=0.3.0
28
+ Requires-Dist: pyquipu-spec~=0.1.1
29
29
  Requires-Dist: textual>=0.58.0
30
30
  Requires-Dist: typer>=0.9.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyquipu-cli"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  authors = [{ name="doucx", email="doucxldh@gmail.com" }]
9
9
  description = "The command-line interface for Quipu."
10
10
  requires-python = ">=3.10"
@@ -25,14 +25,14 @@ classifiers = [
25
25
  keywords = ["quipu", "devops", "cli", "tui", "git", "history"]
26
26
 
27
27
  dependencies = [
28
- "pyquipu-spec ~= 0.1.0",
29
- "pyquipu-engine ~= 0.2.0",
30
- "pyquipu-runtime ~= 0.2.0",
31
- "pyquipu-common ~= 0.1.1",
32
- "pyquipu-application ~= 0.2.0",
33
- "pyquipu-bus ~= 0.2.0",
28
+ "pyquipu-spec ~= 0.1.1",
29
+ "pyquipu-engine ~= 0.3.0",
30
+ "pyquipu-runtime ~= 0.3.0",
31
+ "pyquipu-common ~= 0.2.0",
32
+ "pyquipu-application ~= 0.3.0",
34
33
  "typer >= 0.9.0",
35
34
  "textual >= 0.58.0",
35
+ "click >= 8.1.0",
36
36
  ]
37
37
 
38
38
  [project.urls]
@@ -46,7 +46,6 @@ pyquipu-engine = { workspace = true }
46
46
  pyquipu-runtime = { workspace = true }
47
47
  pyquipu-common = { workspace = true }
48
48
  pyquipu-application = { workspace = true }
49
- pyquipu-bus = { workspace = true }
50
49
 
51
50
  [project.scripts]
52
51
  quipu = "quipu.cli.main:app"
@@ -5,7 +5,8 @@ from pathlib import Path
5
5
  from typing import Annotated, Optional
6
6
 
7
7
  import typer
8
- from quipu.bus import bus
8
+ from needle.pointer import L
9
+ from quipu.common.bus import bus
9
10
 
10
11
  from ..config import DEFAULT_ENTRY_FILE, DEFAULT_WORK_DIR
11
12
  from ..logger_config import setup_logging
@@ -39,13 +40,13 @@ def register(app: typer.Typer):
39
40
  if list_acts:
40
41
  from quipu.application.controller import get_available_acts
41
42
 
42
- bus.info("axon.listActs.ui.header")
43
+ bus.info(L.axon.listActs.ui.header)
43
44
  acts = get_available_acts(work_dir)
44
45
  for name in sorted(acts.keys()):
45
46
  doc = acts[name]
46
47
  clean_doc = inspect.cleandoc(doc) if doc else "暂无说明"
47
48
  indented_doc = "\n".join(f" {line}" for line in clean_doc.splitlines())
48
- item_header = bus.get("axon.listActs.ui.actItem", name=name)
49
+ item_header = bus.render_to_string(L.axon.listActs.ui.actItem, name=name)
49
50
  bus.data(f"{item_header}\n{indented_doc}\n")
50
51
  ctx.exit(0)
51
52
 
@@ -56,7 +57,7 @@ def register(app: typer.Typer):
56
57
  source_desc = ""
57
58
  if file:
58
59
  if not file.exists():
59
- bus.error("common.error.fileNotFound", path=file)
60
+ bus.error(L.common.error.fileNotFound, path=file)
60
61
  ctx.exit(1)
61
62
  content = file.read_text(encoding="utf-8")
62
63
  source_desc = f"文件 ({file.name})"
@@ -74,7 +75,7 @@ def register(app: typer.Typer):
74
75
  source_desc = f"默认文件 ({DEFAULT_ENTRY_FILE.name})"
75
76
 
76
77
  if not content.strip():
77
- bus.warning("axon.warning.noInput")
78
+ bus.warning(L.axon.warning.noInput)
78
79
  ctx.exit(0)
79
80
 
80
81
  # --- 2. 委托给应用层服务 (核心重构) ---
@@ -3,7 +3,8 @@ from pathlib import Path
3
3
  from typing import Annotated
4
4
 
5
5
  import typer
6
- from quipu.bus import bus
6
+ from needle.pointer import L
7
+ from quipu.common.bus import bus
7
8
 
8
9
  from ..config import DEFAULT_WORK_DIR
9
10
  from ..logger_config import setup_logging
@@ -24,14 +25,14 @@ def cache_sync(
24
25
  ),
25
26
  ] = DEFAULT_WORK_DIR,
26
27
  ):
27
- bus.info("cache.sync.info.hydrating")
28
+ bus.info(L.cache.sync.info.hydrating)
28
29
  try:
29
30
  with engine_context(work_dir):
30
31
  pass
31
- bus.success("cache.sync.success")
32
+ bus.success(L.cache.sync.success)
32
33
  except Exception as e:
33
34
  logger.error("数据同步失败", exc_info=True)
34
- bus.error("cache.sync.error", error=str(e))
35
+ bus.error(L.cache.sync.error, error=str(e))
35
36
  ctx.exit(1)
36
37
 
37
38
 
@@ -49,7 +50,7 @@ def cache_rebuild(
49
50
  setup_logging()
50
51
  db_path = work_dir.resolve() / ".quipu" / "history.sqlite"
51
52
  if not db_path.exists():
52
- bus.warning("cache.rebuild.info.dbNotFound")
53
+ bus.warning(L.cache.rebuild.info.dbNotFound)
53
54
  cache_sync(ctx, work_dir)
54
55
 
55
56
 
@@ -66,12 +67,12 @@ def cache_prune_refs(
66
67
  setup_logging()
67
68
 
68
69
  with engine_context(work_dir) as engine:
69
- bus.info("cache.prune.info.scanning")
70
+ bus.info(L.cache.prune.info.scanning)
70
71
 
71
72
  # 1. 获取所有本地 heads
72
73
  local_heads = engine.git_db.get_all_ref_heads("refs/quipu/local/heads/")
73
74
  if not local_heads:
74
- bus.success("cache.prune.info.noRedundant")
75
+ bus.success(L.cache.prune.info.noRedundant)
75
76
  return
76
77
 
77
78
  head_commits = {h[0] for h in local_heads}
@@ -93,7 +94,7 @@ def cache_prune_refs(
93
94
  redundant_commits = head_commits.intersection(parents_of_heads)
94
95
 
95
96
  if not redundant_commits:
96
- bus.success("cache.prune.info.noRedundant")
97
+ bus.success(L.cache.prune.info.noRedundant)
97
98
  return
98
99
 
99
100
  # 4. 找出对应的 ref names 并删除
@@ -102,12 +103,12 @@ def cache_prune_refs(
102
103
  if c_hash in redundant_commits:
103
104
  refs_to_delete.append(ref_name)
104
105
 
105
- bus.info("cache.prune.info.found", count=len(refs_to_delete), total=len(local_heads))
106
+ bus.info(L.cache.prune.info.found, count=len(refs_to_delete), total=len(local_heads))
106
107
 
107
108
  deleted_count = 0
108
109
  for ref in refs_to_delete:
109
110
  engine.git_db.delete_ref(ref)
110
111
  deleted_count += 1
111
112
 
112
- bus.success("cache.prune.success", count=deleted_count)
113
+ bus.success(L.cache.prune.success, count=deleted_count)
113
114
  return
@@ -6,7 +6,8 @@ from typing import Annotated, Dict, List, Optional, Set
6
6
 
7
7
  import typer
8
8
  import yaml
9
- from quipu.bus import bus
9
+ from needle.pointer import L
10
+ from quipu.common.bus import bus
10
11
  from quipu.engine.state_machine import Engine
11
12
  from quipu.spec.models.graph import QuipuNode
12
13
 
@@ -157,7 +158,7 @@ def register(app: typer.Typer):
157
158
 
158
159
  with engine_context(work_dir) as engine:
159
160
  if not engine.history_graph:
160
- bus.info("export.info.emptyHistory")
161
+ bus.info(L.export.info.emptyHistory)
161
162
  ctx.exit(0)
162
163
 
163
164
  nodes_to_process = sorted(engine.history_graph.values(), key=lambda n: n.timestamp, reverse=True)
@@ -171,22 +172,22 @@ def register(app: typer.Typer):
171
172
  filtered = filter_nodes(nodes_to_process, limit, since, until)
172
173
  nodes_to_export = list(reversed(filtered))
173
174
  except typer.BadParameter as e:
174
- bus.error("export.error.badParam", error=str(e))
175
+ bus.error(L.export.error.badParam, error=str(e))
175
176
  ctx.exit(1)
176
177
 
177
178
  if not nodes_to_export:
178
- bus.info("export.info.noMatchingNodes")
179
+ bus.info(L.export.info.noMatchingNodes)
179
180
  ctx.exit(0)
180
181
 
181
182
  if output_dir.exists() and any(output_dir.iterdir()):
182
- prompt = bus.get("export.prompt.overwrite", path=output_dir)
183
+ prompt = bus.render_to_string(L.export.prompt.overwrite, path=output_dir)
183
184
  if not prompt_for_confirmation(prompt, default=False):
184
- bus.warning("common.prompt.cancel")
185
+ bus.warning(L.common.prompt.cancel)
185
186
  raise typer.Abort()
186
187
  shutil.rmtree(output_dir)
187
188
  output_dir.mkdir(parents=True, exist_ok=True)
188
189
 
189
- bus.info("export.info.starting", count=len(nodes_to_export), path=output_dir)
190
+ bus.info(L.export.info.starting, count=len(nodes_to_export), path=output_dir)
190
191
 
191
192
  # 预计算文件名和节点集合以供导航栏使用
192
193
  filename_map = {node.commit_hash: _generate_filename(node) for node in nodes_to_export}
@@ -201,9 +202,9 @@ def register(app: typer.Typer):
201
202
  (output_dir / filename).write_text(content, encoding="utf-8")
202
203
 
203
204
  if zip_output:
204
- bus.info("export.info.zipping")
205
+ bus.info(L.export.info.zipping)
205
206
  zip_path = shutil.make_archive(str(output_dir), "zip", output_dir)
206
207
  shutil.rmtree(output_dir)
207
- bus.success("export.success.zip", path=zip_path)
208
+ bus.success(L.export.success.zip, path=zip_path)
208
209
  else:
209
- bus.success("export.success.dir")
210
+ bus.success(L.export.success.dir)
@@ -6,7 +6,8 @@ from typing import Dict, Generator, List, Optional
6
6
 
7
7
  import typer
8
8
  from quipu.application.factory import create_engine
9
- from quipu.bus import bus
9
+ from needle.pointer import L
10
+ from quipu.common.bus import bus
10
11
  from quipu.engine.state_machine import Engine
11
12
  from quipu.spec.models.graph import QuipuNode
12
13
 
@@ -34,8 +35,8 @@ def _find_current_node(engine: Engine, graph: Dict[str, QuipuNode]) -> Optional[
34
35
  if node.output_tree == current_hash:
35
36
  return node
36
37
 
37
- bus.warning("navigation.warning.workspaceDirty")
38
- bus.info("navigation.info.saveHint")
38
+ bus.warning(L.navigation.warning.workspaceDirty)
39
+ bus.info(L.navigation.info.saveHint)
39
40
  return None
40
41
 
41
42
 
@@ -43,10 +44,10 @@ def _execute_visit(ctx: typer.Context, engine: Engine, target_hash: str, msg_id:
43
44
  bus.info(msg_id, **kwargs)
44
45
  try:
45
46
  engine.visit(target_hash)
46
- bus.success("navigation.success.visit", short_hash=target_hash[:7])
47
+ bus.success(L.navigation.success.visit, short_hash=target_hash[:7])
47
48
  except Exception as e:
48
49
  logger.error(f"导航操作失败 (目标哈希: {target_hash[:12]})", exc_info=True)
49
- bus.error("navigation.error.generic", error=str(e))
50
+ bus.error(L.navigation.error.generic, error=str(e))
50
51
  ctx.exit(1)
51
52
 
52
53
 
@@ -3,7 +3,8 @@ from pathlib import Path
3
3
  from typing import Annotated
4
4
 
5
5
  import typer
6
- from quipu.bus import bus
6
+ from needle.pointer import L
7
+ from quipu.common.bus import bus
7
8
 
8
9
  from ..config import DEFAULT_WORK_DIR
9
10
  from ..ui_utils import prompt_for_confirmation
@@ -28,38 +29,48 @@ def register(app: typer.Typer):
28
29
  with engine_context(work_dir) as engine:
29
30
  graph = engine.history_graph
30
31
 
31
- matches = [node for node in graph.values() if node.output_tree.startswith(hash_prefix)]
32
+ matches = [
33
+ node
34
+ for node in graph.values()
35
+ if node.commit_hash.startswith(hash_prefix) or node.output_tree.startswith(hash_prefix)
36
+ ]
32
37
  if not matches:
33
- bus.error("navigation.checkout.error.notFound", hash_prefix=hash_prefix)
38
+ bus.error(L.navigation.checkout.error.notFound, hash_prefix=hash_prefix)
34
39
  ctx.exit(1)
35
- if len(matches) > 1:
36
- bus.error("navigation.checkout.error.notUnique", hash_prefix=hash_prefix, count=len(matches))
40
+
41
+ unique_output_trees = {node.output_tree for node in matches}
42
+ if len(unique_output_trees) > 1:
43
+ bus.error(L.navigation.checkout.error.notUnique, hash_prefix=hash_prefix, count=len(matches))
37
44
  ctx.exit(1)
45
+
46
+ if len(matches) > 1:
47
+ matches.sort(key=lambda n: (1 if n.parent else 0, n.timestamp), reverse=True)
48
+
38
49
  target_node = matches[0]
39
50
  target_output_tree_hash = target_node.output_tree
40
51
 
41
52
  current_hash = engine.git_db.get_tree_hash()
42
53
  if current_hash == target_output_tree_hash:
43
- bus.success("navigation.checkout.info.noAction", short_hash=target_node.short_hash)
54
+ bus.success(L.navigation.checkout.info.noAction, short_hash=target_node.short_hash)
44
55
  ctx.exit(0)
45
56
 
46
57
  is_dirty = engine.current_node is None or engine.current_node.output_tree != current_hash
47
58
  if is_dirty:
48
- bus.warning("navigation.checkout.info.capturingDrift")
59
+ bus.warning(L.navigation.checkout.info.capturingDrift)
49
60
  engine.capture_drift(current_hash)
50
- bus.success("navigation.checkout.success.driftCaptured")
61
+ bus.success(L.navigation.checkout.success.driftCaptured)
51
62
  current_hash = engine.git_db.get_tree_hash()
52
63
 
53
64
  diff_stat_str = engine.git_db.get_diff_stat(current_hash, target_output_tree_hash)
54
65
 
55
66
  if not force:
56
- prompt = bus.get(
57
- "navigation.checkout.prompt.confirm",
67
+ prompt = bus.render_to_string(
68
+ L.navigation.checkout.prompt.confirm,
58
69
  short_hash=target_node.short_hash,
59
70
  timestamp=target_node.timestamp,
60
71
  )
61
72
  if not prompt_for_confirmation(prompt, diff_lines=diff_stat_str.splitlines(), default=False):
62
- bus.warning("common.prompt.cancel")
73
+ bus.warning(L.common.prompt.cancel)
63
74
  raise typer.Abort()
64
75
 
65
76
  _execute_visit(
@@ -85,9 +96,9 @@ def register(app: typer.Typer):
85
96
  for i in range(count):
86
97
  if not target_node.parent:
87
98
  if i > 0:
88
- bus.success("navigation.undo.reachedRoot", steps=i)
99
+ bus.success(L.navigation.undo.reachedRoot, steps=i)
89
100
  else:
90
- bus.success("navigation.undo.atRoot")
101
+ bus.success(L.navigation.undo.atRoot)
91
102
  if target_node == current_node:
92
103
  ctx.exit(0)
93
104
  break
@@ -116,15 +127,15 @@ def register(app: typer.Typer):
116
127
  for i in range(count):
117
128
  if not target_node.children:
118
129
  if i > 0:
119
- bus.success("navigation.redo.reachedEnd", steps=i)
130
+ bus.success(L.navigation.redo.reachedEnd, steps=i)
120
131
  else:
121
- bus.success("navigation.redo.atEnd")
132
+ bus.success(L.navigation.redo.atEnd)
122
133
  if target_node == current_node:
123
134
  ctx.exit(0)
124
135
  break
125
136
  target_node = target_node.children[-1]
126
137
  if len(current_node.children) > 1:
127
- bus.info("navigation.redo.info.multiBranch", short_hash=target_node.short_hash)
138
+ bus.info(L.navigation.redo.info.multiBranch, short_hash=target_node.short_hash)
128
139
 
129
140
  _execute_visit(
130
141
  ctx,
@@ -146,12 +157,12 @@ def register(app: typer.Typer):
146
157
  ctx.exit(1)
147
158
  siblings = current_node.siblings
148
159
  if len(siblings) <= 1:
149
- bus.success("navigation.prev.noSiblings")
160
+ bus.success(L.navigation.prev.noSiblings)
150
161
  ctx.exit(0)
151
162
  try:
152
163
  idx = siblings.index(current_node)
153
164
  if idx == 0:
154
- bus.success("navigation.prev.atOldest")
165
+ bus.success(L.navigation.prev.atOldest)
155
166
  ctx.exit(0)
156
167
  target_node = siblings[idx - 1]
157
168
  _execute_visit(
@@ -176,12 +187,12 @@ def register(app: typer.Typer):
176
187
  ctx.exit(1)
177
188
  siblings = current_node.siblings
178
189
  if len(siblings) <= 1:
179
- bus.success("navigation.next.noSiblings")
190
+ bus.success(L.navigation.next.noSiblings)
180
191
  ctx.exit(0)
181
192
  try:
182
193
  idx = siblings.index(current_node)
183
194
  if idx == len(siblings) - 1:
184
- bus.success("navigation.next.atNewest")
195
+ bus.success(L.navigation.next.atNewest)
185
196
  ctx.exit(0)
186
197
  target_node = siblings[idx + 1]
187
198
  _execute_visit(
@@ -203,12 +214,12 @@ def register(app: typer.Typer):
203
214
  try:
204
215
  result_hash = engine.back()
205
216
  if result_hash:
206
- bus.success("navigation.back.success", short_hash=result_hash[:7])
217
+ bus.success(L.navigation.back.success, short_hash=result_hash[:7])
207
218
  else:
208
- bus.warning("navigation.back.atStart")
219
+ bus.warning(L.navigation.back.atStart)
209
220
  except Exception as e:
210
221
  logger.error("后退操作失败", exc_info=True)
211
- bus.error("navigation.back.error", error=str(e))
222
+ bus.error(L.navigation.back.error, error=str(e))
212
223
  ctx.exit(1)
213
224
 
214
225
  @app.command(help="在访问历史中前进一步。")
@@ -220,10 +231,10 @@ def register(app: typer.Typer):
220
231
  try:
221
232
  result_hash = engine.forward()
222
233
  if result_hash:
223
- bus.success("navigation.forward.success", short_hash=result_hash[:7])
234
+ bus.success(L.navigation.forward.success, short_hash=result_hash[:7])
224
235
  else:
225
- bus.warning("navigation.forward.atEnd")
236
+ bus.warning(L.navigation.forward.atEnd)
226
237
  except Exception as e:
227
238
  logger.error("前进操作失败", exc_info=True)
228
- bus.error("navigation.forward.error", error=str(e))
239
+ bus.error(L.navigation.forward.error, error=str(e))
229
240
  ctx.exit(1)
@@ -5,7 +5,8 @@ from pathlib import Path
5
5
  from typing import Annotated, List, Optional
6
6
 
7
7
  import typer
8
- from quipu.bus import bus
8
+ from needle.pointer import L
9
+ from quipu.common.bus import bus
9
10
  from quipu.spec.models.graph import QuipuNode
10
11
 
11
12
  from ..config import DEFAULT_WORK_DIR
@@ -58,7 +59,7 @@ def register(app: typer.Typer):
58
59
  if json_output:
59
60
  bus.data("[]")
60
61
  else:
61
- bus.info("query.info.emptyHistory")
62
+ bus.info(L.query.info.emptyHistory)
62
63
  raise typer.Exit(0)
63
64
 
64
65
  nodes_to_process = sorted(graph.values(), key=lambda n: n.timestamp, reverse=True)
@@ -69,21 +70,21 @@ def register(app: typer.Typer):
69
70
  try:
70
71
  nodes = filter_nodes(nodes_to_process, limit, since, until)
71
72
  except typer.BadParameter as e:
72
- bus.error("common.error.invalidConfig", error=str(e))
73
+ bus.error(L.common.error.invalidConfig, error=str(e))
73
74
  ctx.exit(1)
74
75
 
75
76
  if not nodes:
76
77
  if json_output:
77
78
  bus.data("[]")
78
79
  else:
79
- bus.info("query.info.noResults")
80
+ bus.info(L.query.info.noResults)
80
81
  raise typer.Exit(0)
81
82
 
82
83
  if json_output:
83
84
  bus.data(_nodes_to_json_str(nodes))
84
85
  raise typer.Exit(0)
85
86
 
86
- bus.info("query.log.ui.header")
87
+ bus.info(L.query.log.ui.header)
87
88
  for node in nodes:
88
89
  ts = node.timestamp.strftime("%Y-%m-%d %H:%M:%S")
89
90
  tag = f"[{node.node_type.upper()}]"
@@ -111,7 +112,7 @@ def register(app: typer.Typer):
111
112
  if json_output:
112
113
  bus.data("[]")
113
114
  else:
114
- bus.info("query.info.emptyHistory")
115
+ bus.info(L.query.info.emptyHistory)
115
116
  ctx.exit(0)
116
117
 
117
118
  nodes = engine.find_nodes(summary_regex=summary_regex, node_type=node_type, limit=limit)
@@ -120,14 +121,14 @@ def register(app: typer.Typer):
120
121
  if json_output:
121
122
  bus.data("[]")
122
123
  else:
123
- bus.info("query.info.noResults")
124
+ bus.info(L.query.info.noResults)
124
125
  ctx.exit(0)
125
126
 
126
127
  if json_output:
127
128
  bus.data(_nodes_to_json_str(nodes))
128
129
  ctx.exit(0)
129
130
 
130
- bus.info("query.find.ui.header")
131
+ bus.info(L.query.find.ui.header)
131
132
  for node in nodes:
132
133
  ts = node.timestamp.strftime("%Y-%m-%d %H:%M:%S")
133
134
  tag = f"[{node.node_type.upper()}]"
@@ -5,7 +5,8 @@ from typing import Annotated, Optional
5
5
 
6
6
  import typer
7
7
  from quipu.application.utils import find_git_repository_root
8
- from quipu.bus import bus
8
+ from needle.pointer import L
9
+ from quipu.common.bus import bus
9
10
  from quipu.common.identity import get_user_id_from_email
10
11
  from quipu.engine.config import ConfigManager
11
12
  from quipu.engine.git_db import GitDB
@@ -52,7 +53,7 @@ def register(app: typer.Typer):
52
53
 
53
54
  final_user_id = config.get("sync.user_id")
54
55
  if not final_user_id:
55
- bus.info("sync.setup.firstUse")
56
+ bus.info(L.sync.setup.firstUse)
56
57
  try:
57
58
  result = subprocess.run(
58
59
  ["git", "config", "user.email"], cwd=sync_dir, capture_output=True, text=True, check=True
@@ -64,11 +65,11 @@ def register(app: typer.Typer):
64
65
  final_user_id = get_user_id_from_email(email)
65
66
  config.set("sync.user_id", final_user_id)
66
67
  config.save()
67
- bus.success("sync.setup.success", email=email, user_id=final_user_id)
68
+ bus.success(L.sync.setup.success, email=email, user_id=final_user_id)
68
69
 
69
70
  except (subprocess.CalledProcessError, ValueError, FileNotFoundError):
70
- bus.error("sync.setup.error.noEmail")
71
- bus.warning("sync.setup.info.emailHint")
71
+ bus.error(L.sync.setup.error.noEmail)
72
+ bus.warning(L.sync.setup.info.emailHint)
72
73
  ctx.exit(1)
73
74
 
74
75
  try:
@@ -77,50 +78,50 @@ def register(app: typer.Typer):
77
78
  target_ids_to_fetch = set(subscriptions)
78
79
  target_ids_to_fetch.add(final_user_id)
79
80
 
80
- bus.info("sync.run.info.mode", mode=mode.value)
81
+ bus.info(L.sync.run.info.mode, mode=mode.value)
81
82
 
82
83
  # --- Operation Dispatch based on Mode ---
83
84
  match mode:
84
85
  case SyncMode.BIDIRECTIONAL:
85
- bus.info("sync.run.info.pulling")
86
+ bus.info(L.sync.run.info.pulling)
86
87
  for target_id in sorted(list(target_ids_to_fetch)):
87
88
  git_db.fetch_quipu_refs(remote, target_id)
88
- bus.info("sync.run.info.reconciling")
89
+ bus.info(L.sync.run.info.reconciling)
89
90
  git_db.reconcile_local_with_remote(remote, final_user_id)
90
- bus.info("sync.run.info.pushing")
91
+ bus.info(L.sync.run.info.pushing)
91
92
  git_db.push_quipu_refs(remote, final_user_id)
92
- bus.success("sync.run.success.bidirectional")
93
+ bus.success(L.sync.run.success.bidirectional)
93
94
 
94
95
  case SyncMode.PULL_ONLY:
95
- bus.info("sync.run.info.pulling")
96
+ bus.info(L.sync.run.info.pulling)
96
97
  for target_id in sorted(list(target_ids_to_fetch)):
97
98
  git_db.fetch_quipu_refs(remote, target_id)
98
- bus.info("sync.run.info.reconciling")
99
+ bus.info(L.sync.run.info.reconciling)
99
100
  git_db.reconcile_local_with_remote(remote, final_user_id)
100
- bus.success("sync.run.success.pullOnly")
101
+ bus.success(L.sync.run.success.pullOnly)
101
102
 
102
103
  case SyncMode.PULL_PRUNE:
103
- bus.info("sync.run.info.pullingPrune")
104
+ bus.info(L.sync.run.info.pullingPrune)
104
105
  for target_id in sorted(list(target_ids_to_fetch)):
105
106
  git_db.fetch_quipu_refs(remote, target_id)
106
- bus.info("sync.run.info.reconciling")
107
+ bus.info(L.sync.run.info.reconciling)
107
108
  git_db.reconcile_local_with_remote(remote, final_user_id)
108
- bus.info("sync.run.info.pruning")
109
+ bus.info(L.sync.run.info.pruning)
109
110
  git_db.prune_local_from_remote(remote, final_user_id)
110
- bus.success("sync.run.success.pullPrune")
111
+ bus.success(L.sync.run.success.pullPrune)
111
112
 
112
113
  case SyncMode.PUSH_ONLY:
113
- bus.info("sync.run.info.pushing")
114
+ bus.info(L.sync.run.info.pushing)
114
115
  git_db.push_quipu_refs(remote, final_user_id, force=False)
115
- bus.success("sync.run.success.pushOnly")
116
+ bus.success(L.sync.run.success.pushOnly)
116
117
 
117
118
  case SyncMode.PUSH_FORCE:
118
- bus.info("sync.run.info.pushingForce")
119
+ bus.info(L.sync.run.info.pushingForce)
119
120
  git_db.push_quipu_refs(remote, final_user_id, force=True)
120
- bus.success("sync.run.success.pushForce")
121
+ bus.success(L.sync.run.success.pushForce)
121
122
 
122
- bus.info("sync.run.info.cacheHint")
123
+ bus.info(L.sync.run.info.cacheHint)
123
124
 
124
125
  except RuntimeError as e:
125
- bus.error("sync.run.error.generic", error=str(e))
126
+ bus.error(L.sync.run.error.generic, error=str(e))
126
127
  ctx.exit(1)