codealmanac 0.1.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. codealmanac/__init__.py +13 -0
  2. codealmanac/app.py +175 -0
  3. codealmanac/cli/__init__.py +1 -0
  4. codealmanac/cli/dispatch/__init__.py +0 -0
  5. codealmanac/cli/dispatch/admin.py +124 -0
  6. codealmanac/cli/dispatch/config.py +50 -0
  7. codealmanac/cli/dispatch/root.py +328 -0
  8. codealmanac/cli/main.py +28 -0
  9. codealmanac/cli/parser/__init__.py +0 -0
  10. codealmanac/cli/parser/admin.py +81 -0
  11. codealmanac/cli/parser/lifecycle.py +57 -0
  12. codealmanac/cli/parser/root.py +19 -0
  13. codealmanac/cli/parser/wiki.py +87 -0
  14. codealmanac/cli/render/__init__.py +0 -0
  15. codealmanac/cli/render/admin.py +191 -0
  16. codealmanac/cli/render/root.py +290 -0
  17. codealmanac/core/__init__.py +1 -0
  18. codealmanac/core/errors.py +45 -0
  19. codealmanac/core/models.py +14 -0
  20. codealmanac/core/paths.py +25 -0
  21. codealmanac/core/slug.py +7 -0
  22. codealmanac/core/text.py +5 -0
  23. codealmanac/database/__init__.py +15 -0
  24. codealmanac/database/sqlite.py +54 -0
  25. codealmanac/integrations/__init__.py +1 -0
  26. codealmanac/integrations/automation/__init__.py +3 -0
  27. codealmanac/integrations/automation/scheduler/__init__.py +5 -0
  28. codealmanac/integrations/automation/scheduler/launchd.py +163 -0
  29. codealmanac/integrations/command.py +56 -0
  30. codealmanac/integrations/harnesses/__init__.py +7 -0
  31. codealmanac/integrations/harnesses/claude/__init__.py +1 -0
  32. codealmanac/integrations/harnesses/claude/adapter.py +217 -0
  33. codealmanac/integrations/harnesses/codex/__init__.py +3 -0
  34. codealmanac/integrations/harnesses/codex/adapter.py +221 -0
  35. codealmanac/integrations/harnesses/git_status.py +49 -0
  36. codealmanac/integrations/sources/__init__.py +29 -0
  37. codealmanac/integrations/sources/filesystem/__init__.py +5 -0
  38. codealmanac/integrations/sources/filesystem/adapter.py +685 -0
  39. codealmanac/integrations/sources/filesystem/selection.py +209 -0
  40. codealmanac/integrations/sources/git/__init__.py +3 -0
  41. codealmanac/integrations/sources/git/adapter.py +132 -0
  42. codealmanac/integrations/sources/github/__init__.py +3 -0
  43. codealmanac/integrations/sources/github/adapter.py +413 -0
  44. codealmanac/integrations/sources/runtime.py +22 -0
  45. codealmanac/integrations/sources/transcripts/__init__.py +33 -0
  46. codealmanac/integrations/sources/transcripts/claude.py +61 -0
  47. codealmanac/integrations/sources/transcripts/codex.py +69 -0
  48. codealmanac/integrations/sources/transcripts/jsonl.py +84 -0
  49. codealmanac/integrations/sources/transcripts/runtime.py +387 -0
  50. codealmanac/integrations/sources/web/__init__.py +3 -0
  51. codealmanac/integrations/sources/web/adapter.py +303 -0
  52. codealmanac/integrations/updates/__init__.py +7 -0
  53. codealmanac/integrations/updates/package.py +85 -0
  54. codealmanac/integrations/workspaces/__init__.py +1 -0
  55. codealmanac/integrations/workspaces/git/__init__.py +3 -0
  56. codealmanac/integrations/workspaces/git/probe.py +128 -0
  57. codealmanac/manual/README.md +24 -0
  58. codealmanac/manual/__init__.py +19 -0
  59. codealmanac/manual/build.md +20 -0
  60. codealmanac/manual/evidence.md +23 -0
  61. codealmanac/manual/garden.md +20 -0
  62. codealmanac/manual/ingest.md +17 -0
  63. codealmanac/manual/library.py +84 -0
  64. codealmanac/manual/models.py +83 -0
  65. codealmanac/manual/pages.md +28 -0
  66. codealmanac/manual/requests.py +6 -0
  67. codealmanac/manual/sources.md +18 -0
  68. codealmanac/manual/style.md +19 -0
  69. codealmanac/prompts/__init__.py +5 -0
  70. codealmanac/prompts/base/notability.md +14 -0
  71. codealmanac/prompts/base/purpose.md +23 -0
  72. codealmanac/prompts/base/syntax.md +19 -0
  73. codealmanac/prompts/models.py +9 -0
  74. codealmanac/prompts/operations/garden.md +26 -0
  75. codealmanac/prompts/operations/ingest.md +18 -0
  76. codealmanac/prompts/renderer.py +24 -0
  77. codealmanac/prompts/requests.py +22 -0
  78. codealmanac/server/__init__.py +1 -0
  79. codealmanac/server/app.py +202 -0
  80. codealmanac/server/assets/__init__.py +1 -0
  81. codealmanac/server/assets/app.css +865 -0
  82. codealmanac/server/assets/app.js +3 -0
  83. codealmanac/server/assets/index.html +80 -0
  84. codealmanac/server/assets/viewer/api.js +30 -0
  85. codealmanac/server/assets/viewer/components.js +197 -0
  86. codealmanac/server/assets/viewer/main.js +126 -0
  87. codealmanac/server/assets/viewer/renderers.js +122 -0
  88. codealmanac/server/assets/viewer/routes.js +36 -0
  89. codealmanac/services/__init__.py +1 -0
  90. codealmanac/services/automation/__init__.py +3 -0
  91. codealmanac/services/automation/models.py +83 -0
  92. codealmanac/services/automation/ports.py +14 -0
  93. codealmanac/services/automation/requests.py +40 -0
  94. codealmanac/services/automation/service.py +294 -0
  95. codealmanac/services/config/__init__.py +17 -0
  96. codealmanac/services/config/models.py +61 -0
  97. codealmanac/services/config/requests.py +21 -0
  98. codealmanac/services/config/service.py +55 -0
  99. codealmanac/services/config/store.py +26 -0
  100. codealmanac/services/diagnostics/__init__.py +1 -0
  101. codealmanac/services/diagnostics/models.py +22 -0
  102. codealmanac/services/diagnostics/requests.py +8 -0
  103. codealmanac/services/diagnostics/service.py +283 -0
  104. codealmanac/services/harnesses/__init__.py +1 -0
  105. codealmanac/services/harnesses/models.py +104 -0
  106. codealmanac/services/harnesses/ports.py +18 -0
  107. codealmanac/services/harnesses/requests.py +19 -0
  108. codealmanac/services/harnesses/service.py +38 -0
  109. codealmanac/services/health/__init__.py +1 -0
  110. codealmanac/services/health/requests.py +8 -0
  111. codealmanac/services/health/service.py +20 -0
  112. codealmanac/services/index/__init__.py +1 -0
  113. codealmanac/services/index/models.py +135 -0
  114. codealmanac/services/index/requests.py +26 -0
  115. codealmanac/services/index/service.py +86 -0
  116. codealmanac/services/index/store.py +411 -0
  117. codealmanac/services/index/views.py +524 -0
  118. codealmanac/services/pages/__init__.py +1 -0
  119. codealmanac/services/pages/requests.py +17 -0
  120. codealmanac/services/pages/service.py +26 -0
  121. codealmanac/services/runs/__init__.py +1 -0
  122. codealmanac/services/runs/models.py +91 -0
  123. codealmanac/services/runs/requests.py +76 -0
  124. codealmanac/services/runs/service.py +86 -0
  125. codealmanac/services/runs/store.py +256 -0
  126. codealmanac/services/search/__init__.py +1 -0
  127. codealmanac/services/search/requests.py +23 -0
  128. codealmanac/services/search/service.py +31 -0
  129. codealmanac/services/sources/__init__.py +1 -0
  130. codealmanac/services/sources/models.py +126 -0
  131. codealmanac/services/sources/ports.py +30 -0
  132. codealmanac/services/sources/requests.py +76 -0
  133. codealmanac/services/sources/service.py +351 -0
  134. codealmanac/services/tagging/__init__.py +1 -0
  135. codealmanac/services/tagging/models.py +9 -0
  136. codealmanac/services/tagging/requests.py +35 -0
  137. codealmanac/services/tagging/service.py +43 -0
  138. codealmanac/services/topics/__init__.py +1 -0
  139. codealmanac/services/topics/models.py +36 -0
  140. codealmanac/services/topics/requests.py +115 -0
  141. codealmanac/services/topics/service.py +297 -0
  142. codealmanac/services/updates/__init__.py +4 -0
  143. codealmanac/services/updates/models.py +83 -0
  144. codealmanac/services/updates/ports.py +17 -0
  145. codealmanac/services/updates/requests.py +10 -0
  146. codealmanac/services/updates/service.py +113 -0
  147. codealmanac/services/viewer/__init__.py +1 -0
  148. codealmanac/services/viewer/models.py +80 -0
  149. codealmanac/services/viewer/renderer.py +89 -0
  150. codealmanac/services/viewer/requests.py +86 -0
  151. codealmanac/services/viewer/service.py +211 -0
  152. codealmanac/services/wiki/__init__.py +1 -0
  153. codealmanac/services/wiki/documents.py +83 -0
  154. codealmanac/services/wiki/frontmatter.py +94 -0
  155. codealmanac/services/wiki/frontmatter_rewrite.py +142 -0
  156. codealmanac/services/wiki/models.py +69 -0
  157. codealmanac/services/wiki/paths.py +42 -0
  158. codealmanac/services/wiki/service.py +57 -0
  159. codealmanac/services/wiki/templates.py +73 -0
  160. codealmanac/services/wiki/topics.py +266 -0
  161. codealmanac/services/wiki/wikilinks.py +58 -0
  162. codealmanac/services/workspaces/__init__.py +1 -0
  163. codealmanac/services/workspaces/models.py +124 -0
  164. codealmanac/services/workspaces/ports.py +9 -0
  165. codealmanac/services/workspaces/requests.py +82 -0
  166. codealmanac/services/workspaces/roots.py +74 -0
  167. codealmanac/services/workspaces/service.py +303 -0
  168. codealmanac/services/workspaces/store.py +127 -0
  169. codealmanac/workflows/__init__.py +1 -0
  170. codealmanac/workflows/build/__init__.py +1 -0
  171. codealmanac/workflows/build/models.py +8 -0
  172. codealmanac/workflows/build/service.py +45 -0
  173. codealmanac/workflows/garden/__init__.py +3 -0
  174. codealmanac/workflows/garden/models.py +30 -0
  175. codealmanac/workflows/garden/requests.py +22 -0
  176. codealmanac/workflows/garden/service.py +239 -0
  177. codealmanac/workflows/ingest/__init__.py +1 -0
  178. codealmanac/workflows/ingest/models.py +26 -0
  179. codealmanac/workflows/ingest/requests.py +39 -0
  180. codealmanac/workflows/ingest/service.py +302 -0
  181. codealmanac/workflows/lifecycle.py +197 -0
  182. codealmanac/workflows/sync/__init__.py +3 -0
  183. codealmanac/workflows/sync/models.py +157 -0
  184. codealmanac/workflows/sync/requests.py +63 -0
  185. codealmanac/workflows/sync/service.py +651 -0
  186. codealmanac/workflows/sync/store.py +51 -0
  187. codealmanac-0.1.0.dev0.dist-info/METADATA +248 -0
  188. codealmanac-0.1.0.dev0.dist-info/RECORD +192 -0
  189. codealmanac-0.1.0.dev0.dist-info/WHEEL +5 -0
  190. codealmanac-0.1.0.dev0.dist-info/entry_points.txt +2 -0
  191. codealmanac-0.1.0.dev0.dist-info/licenses/LICENSE.md +201 -0
  192. codealmanac-0.1.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,28 @@
1
+ import argparse
2
+ import sys
3
+ from collections.abc import Sequence
4
+
5
+ from pydantic import ValidationError
6
+
7
+ from codealmanac.app import create_app
8
+ from codealmanac.cli.dispatch.root import dispatch as dispatch_app
9
+ from codealmanac.cli.parser.root import build_parser
10
+ from codealmanac.core.errors import CodeAlmanacError
11
+
12
+
13
+ def main(argv: Sequence[str] | None = None) -> int:
14
+ parser = build_parser()
15
+ args = parser.parse_args(argv)
16
+ try:
17
+ return dispatch(args)
18
+ except (CodeAlmanacError, ValidationError) as error:
19
+ print(f"codealmanac: {error}", file=sys.stderr)
20
+ return 1
21
+
22
+
23
+ def dispatch(args: argparse.Namespace) -> int:
24
+ return dispatch_app(args, create_app())
25
+
26
+
27
+ if __name__ == "__main__":
28
+ raise SystemExit(main())
File without changes
@@ -0,0 +1,81 @@
1
+ import argparse
2
+
3
+ from codealmanac.services.automation.models import AutomationTask
4
+
5
+
6
+ def add_admin_commands(subcommands: argparse._SubParsersAction) -> None:
7
+ doctor = subcommands.add_parser("doctor", help="check local install and wiki")
8
+ doctor.add_argument("--wiki")
9
+ doctor.add_argument("--json", action="store_true")
10
+
11
+ update = subcommands.add_parser("update", help="update the local CLI")
12
+ update.add_argument("--check", action="store_true")
13
+ update.add_argument("--json", action="store_true")
14
+
15
+ jobs = subcommands.add_parser("jobs", help="inspect local lifecycle jobs")
16
+ jobs.add_argument("--wiki")
17
+ jobs.add_argument("--limit", type=int)
18
+ jobs.add_argument("--json", action="store_true")
19
+ job_subcommands = jobs.add_subparsers(dest="jobs_command")
20
+ jobs_show = job_subcommands.add_parser("show", help="show one job record")
21
+ jobs_show.add_argument("run_id")
22
+ jobs_show.add_argument("--json", action="store_true")
23
+ jobs_logs = job_subcommands.add_parser("logs", help="show one job log")
24
+ jobs_logs.add_argument("run_id")
25
+ jobs_logs.add_argument("--json", action="store_true")
26
+
27
+ automation = subcommands.add_parser(
28
+ "automation",
29
+ help="manage local scheduled automation",
30
+ )
31
+ automation_subcommands = automation.add_subparsers(
32
+ dest="automation_command",
33
+ required=True,
34
+ )
35
+ automation_install = automation_subcommands.add_parser(
36
+ "install",
37
+ help="install scheduled sync and garden jobs",
38
+ )
39
+ automation_install.add_argument(
40
+ "tasks",
41
+ nargs="*",
42
+ choices=tuple(task.value for task in AutomationTask),
43
+ )
44
+ automation_install.add_argument(
45
+ "--every",
46
+ help="run interval for sync or one selected task",
47
+ )
48
+ automation_install.add_argument(
49
+ "--quiet",
50
+ help="minimum quiet time before sync",
51
+ )
52
+ automation_install.add_argument(
53
+ "--garden-every",
54
+ help="Garden run interval (default: 4h)",
55
+ )
56
+ automation_install.add_argument(
57
+ "--garden-off",
58
+ action="store_true",
59
+ help="disable scheduled Garden automation",
60
+ )
61
+ automation_install.add_argument("--json", action="store_true")
62
+ automation_uninstall = automation_subcommands.add_parser(
63
+ "uninstall",
64
+ help="remove scheduled jobs",
65
+ )
66
+ automation_uninstall.add_argument(
67
+ "tasks",
68
+ nargs="*",
69
+ choices=tuple(task.value for task in AutomationTask),
70
+ )
71
+ automation_uninstall.add_argument("--json", action="store_true")
72
+ automation_status = automation_subcommands.add_parser(
73
+ "status",
74
+ help="show scheduled automation status",
75
+ )
76
+ automation_status.add_argument(
77
+ "tasks",
78
+ nargs="*",
79
+ choices=tuple(task.value for task in AutomationTask),
80
+ )
81
+ automation_status.add_argument("--json", action="store_true")
@@ -0,0 +1,57 @@
1
+ import argparse
2
+
3
+ from codealmanac.services.harnesses.models import HarnessKind
4
+
5
+
6
+ def add_lifecycle_commands(subcommands: argparse._SubParsersAction) -> None:
7
+ init = subcommands.add_parser("init", help="initialize a local Almanac wiki")
8
+ init.add_argument("path", nargs="?", default=".")
9
+ init.add_argument("--root")
10
+ init.add_argument("--name")
11
+ init.add_argument("--description", default="")
12
+
13
+ build = subcommands.add_parser("build", help="build or refresh a local wiki")
14
+ build.add_argument("path", nargs="?", default=".")
15
+ build.add_argument("--root")
16
+ build.add_argument("--name")
17
+ build.add_argument("--description", default="")
18
+
19
+ ingest = subcommands.add_parser("ingest", help="ingest local material")
20
+ ingest.add_argument("inputs", nargs="+")
21
+ ingest.add_argument("--wiki")
22
+ ingest.add_argument(
23
+ "--using",
24
+ choices=tuple(kind.value for kind in HarnessKind),
25
+ )
26
+ ingest.add_argument("--title")
27
+ ingest.add_argument("--guidance")
28
+
29
+ garden = subcommands.add_parser("garden", help="garden the local wiki")
30
+ garden.add_argument("--wiki")
31
+ garden.add_argument(
32
+ "--using",
33
+ choices=tuple(kind.value for kind in HarnessKind),
34
+ )
35
+ garden.add_argument("--title")
36
+ garden.add_argument("--guidance")
37
+
38
+ sync = subcommands.add_parser("sync", help="sync quiet local transcripts")
39
+ sync.add_argument("--wiki")
40
+ sync.add_argument("--from", dest="source_apps")
41
+ sync.add_argument("--quiet")
42
+ sync.add_argument("--pending-timeout")
43
+ sync.add_argument("--max-failed-attempts", type=int)
44
+ sync.add_argument("--claim-owner")
45
+ sync.add_argument(
46
+ "--using",
47
+ choices=tuple(kind.value for kind in HarnessKind),
48
+ )
49
+ sync.add_argument("--json", action="store_true")
50
+ sync_subcommands = sync.add_subparsers(dest="sync_command")
51
+ sync_status = sync_subcommands.add_parser("status", help="show sync readiness")
52
+ sync_status.add_argument("--wiki")
53
+ sync_status.add_argument("--from", dest="source_apps")
54
+ sync_status.add_argument("--quiet")
55
+ sync_status.add_argument("--pending-timeout")
56
+ sync_status.add_argument("--max-failed-attempts", type=int)
57
+ sync_status.add_argument("--json", action="store_true")
@@ -0,0 +1,19 @@
1
+ import argparse
2
+
3
+ from codealmanac import __version__
4
+ from codealmanac.cli.parser.admin import add_admin_commands
5
+ from codealmanac.cli.parser.lifecycle import add_lifecycle_commands
6
+ from codealmanac.cli.parser.wiki import add_wiki_commands
7
+
8
+
9
+ def build_parser() -> argparse.ArgumentParser:
10
+ parser = argparse.ArgumentParser(
11
+ prog="codealmanac",
12
+ description="Maintain a local Almanac wiki for a codebase.",
13
+ )
14
+ parser.add_argument("--version", action="version", version=__version__)
15
+ subcommands = parser.add_subparsers(dest="command", required=True)
16
+ add_lifecycle_commands(subcommands)
17
+ add_wiki_commands(subcommands)
18
+ add_admin_commands(subcommands)
19
+ return parser
@@ -0,0 +1,87 @@
1
+ import argparse
2
+
3
+ DEFAULT_VIEWER_HOST = "127.0.0.1"
4
+ DEFAULT_VIEWER_PORT = 3927
5
+
6
+
7
+ def add_wiki_commands(subcommands: argparse._SubParsersAction) -> None:
8
+ list_parser = subcommands.add_parser("list", help="list registered local wikis")
9
+ list_parser.add_argument("--json", action="store_true")
10
+ list_actions = list_parser.add_mutually_exclusive_group()
11
+ list_actions.add_argument("--drop")
12
+ list_actions.add_argument("--drop-missing", action="store_true")
13
+
14
+ search = subcommands.add_parser("search", help="search the local wiki")
15
+ search.add_argument("query", nargs="?")
16
+ search.add_argument("--wiki")
17
+ search.add_argument("--topic", action="append", default=[])
18
+ search.add_argument("--mentions")
19
+ search.add_argument("--include-archive", action="store_true")
20
+ search.add_argument("--archived", action="store_true")
21
+ search.add_argument("--limit", type=int)
22
+ search.add_argument("--json", action="store_true")
23
+
24
+ show = subcommands.add_parser("show", help="show a wiki page")
25
+ show.add_argument("slug")
26
+ show.add_argument("--wiki")
27
+ show.add_argument("--json", action="store_true")
28
+ show.add_argument("--body", action="store_true")
29
+ show.add_argument("--meta", action="store_true")
30
+ show.add_argument("--lead", action="store_true")
31
+ show.add_argument("--links", action="store_true")
32
+ show.add_argument("--backlinks", action="store_true")
33
+ show.add_argument("--files", action="store_true")
34
+ show.add_argument("--topics", action="store_true")
35
+
36
+ topics = subcommands.add_parser("topics", help="list or inspect topics")
37
+ topics.add_argument("--wiki")
38
+ topic_subcommands = topics.add_subparsers(dest="topic_command")
39
+ topic_show = topic_subcommands.add_parser("show", help="show a topic")
40
+ topic_show.add_argument("slug")
41
+ topic_show.add_argument("--descendants", action="store_true")
42
+ topic_create = topic_subcommands.add_parser("create", help="create a topic")
43
+ topic_create.add_argument("name")
44
+ topic_create.add_argument("--parent", action="append", default=[])
45
+ topic_describe = topic_subcommands.add_parser(
46
+ "describe",
47
+ help="set a topic description",
48
+ )
49
+ topic_describe.add_argument("slug")
50
+ topic_describe.add_argument("description")
51
+ topic_link = topic_subcommands.add_parser("link", help="link topic to parent")
52
+ topic_link.add_argument("child")
53
+ topic_link.add_argument("parent")
54
+ topic_unlink = topic_subcommands.add_parser(
55
+ "unlink",
56
+ help="unlink topic from parent",
57
+ )
58
+ topic_unlink.add_argument("child")
59
+ topic_unlink.add_argument("parent")
60
+ topic_rename = topic_subcommands.add_parser("rename", help="rename a topic")
61
+ topic_rename.add_argument("old_slug")
62
+ topic_rename.add_argument("new_slug")
63
+ topic_delete = topic_subcommands.add_parser("delete", help="delete a topic")
64
+ topic_delete.add_argument("slug")
65
+
66
+ health = subcommands.add_parser("health", help="check wiki health")
67
+ health.add_argument("--wiki")
68
+ health.add_argument("--json", action="store_true")
69
+
70
+ reindex = subcommands.add_parser("reindex", help="force a full index rebuild")
71
+ reindex.add_argument("--wiki")
72
+ reindex.add_argument("--json", action="store_true")
73
+
74
+ serve = subcommands.add_parser("serve", help="serve the local wiki viewer")
75
+ serve.add_argument("--wiki")
76
+ serve.add_argument("--host", default=DEFAULT_VIEWER_HOST)
77
+ serve.add_argument("--port", type=int, default=DEFAULT_VIEWER_PORT)
78
+
79
+ tag = subcommands.add_parser("tag", help="add topics to a page")
80
+ tag.add_argument("slug")
81
+ tag.add_argument("topics", nargs="+")
82
+ tag.add_argument("--wiki")
83
+
84
+ untag = subcommands.add_parser("untag", help="remove topics from a page")
85
+ untag.add_argument("slug")
86
+ untag.add_argument("topics", nargs="+")
87
+ untag.add_argument("--wiki")
File without changes
@@ -0,0 +1,191 @@
1
+ import json
2
+ import shlex
3
+ import sys
4
+ from datetime import timedelta
5
+
6
+ from codealmanac.services.automation.models import (
7
+ AutomationInstallResult,
8
+ AutomationStatusReport,
9
+ AutomationTask,
10
+ AutomationUninstallResult,
11
+ ScheduledJob,
12
+ ScheduledJobStatus,
13
+ )
14
+ from codealmanac.services.diagnostics.models import DoctorCheck, DoctorReport
15
+ from codealmanac.services.runs.models import RunLogEvent, RunRecord
16
+ from codealmanac.services.updates.models import UpdatePlan, UpdateResult
17
+
18
+
19
+ def render_automation_install(
20
+ result: AutomationInstallResult,
21
+ json_output: bool,
22
+ ) -> None:
23
+ if json_output:
24
+ print(json.dumps(result.model_dump(mode="json"), indent=2))
25
+ return
26
+ print("automation installed")
27
+ for job in result.jobs:
28
+ print_automation_job(job)
29
+ for job in result.disabled:
30
+ print(f" {job.task.value}: disabled")
31
+
32
+
33
+ def render_automation_uninstall(
34
+ result: AutomationUninstallResult,
35
+ json_output: bool,
36
+ ) -> None:
37
+ if json_output:
38
+ print(json.dumps(result.model_dump(mode="json"), indent=2))
39
+ return
40
+ if len(result.removed) == 0:
41
+ print("automation not installed")
42
+ return
43
+ print("automation removed")
44
+ for path in result.removed:
45
+ print(f" plist: {path}")
46
+
47
+
48
+ def render_automation_status(
49
+ report: AutomationStatusReport,
50
+ json_output: bool,
51
+ ) -> None:
52
+ if json_output:
53
+ print(json.dumps(report.model_dump(mode="json"), indent=2))
54
+ return
55
+ for status in report.statuses:
56
+ render_automation_job_status(status)
57
+
58
+
59
+ def print_automation_job(job: ScheduledJob) -> None:
60
+ print(f" {job.task.value} interval: {duration_label(job.interval)}")
61
+ if job.task == AutomationTask.SYNC:
62
+ quiet = job.program_arguments[job.program_arguments.index("--quiet") + 1]
63
+ print(f" sync quiet: {quiet}")
64
+ print(f" {job.task.value} command: {' '.join(job.program_arguments)}")
65
+ if job.working_directory is not None:
66
+ print(f" {job.task.value} cwd: {job.working_directory}")
67
+ print(f" {job.task.value} plist: {job.plist_path}")
68
+
69
+
70
+ def render_automation_job_status(status: ScheduledJobStatus) -> None:
71
+ label = f"{status.task.value} automation"
72
+ if not status.installed:
73
+ print(f"{label}: not installed")
74
+ return
75
+ print(f"{label}: installed")
76
+ print(f" plist: {status.plist_path}")
77
+ print(f" launchd loaded: {'yes' if status.loaded else 'no'}")
78
+ if status.interval is not None:
79
+ print(f" interval: {duration_label(status.interval)}")
80
+ if status.quiet is not None:
81
+ print(f" quiet: {duration_label(status.quiet)}")
82
+
83
+
84
+ def duration_label(value: timedelta) -> str:
85
+ seconds = int(value.total_seconds())
86
+ return f"{seconds}s"
87
+
88
+
89
+ def render_doctor(report: DoctorReport, json_output: bool) -> None:
90
+ if json_output:
91
+ print(json.dumps(report.model_dump(mode="json"), indent=2))
92
+ return
93
+ print(f"codealmanac v{report.version}")
94
+ print("")
95
+ render_doctor_section("Install", report.install)
96
+ render_doctor_section("Current wiki", report.wiki)
97
+
98
+
99
+ def render_doctor_section(title: str, checks: tuple[DoctorCheck, ...]) -> None:
100
+ if len(checks) == 0:
101
+ return
102
+ print(f"## {title}")
103
+ for check in checks:
104
+ print(f" {check.status.value} {check.message}")
105
+ if check.fix is not None:
106
+ print(f" {check.fix}")
107
+ print("")
108
+
109
+
110
+ def render_update_plan(plan: UpdatePlan, json_output: bool) -> None:
111
+ if json_output:
112
+ print(json.dumps(plan.model_dump(mode="json"), indent=2))
113
+ return
114
+ print(f"codealmanac {plan.installed_version}")
115
+ print(f"update status: {plan.status.value}")
116
+ print(f"install method: {plan.method.value}")
117
+ print(f"message: {plan.message}")
118
+ if plan.command:
119
+ print(f"command: {shell_command(plan.command)}")
120
+ if plan.fix is not None:
121
+ print(plan.fix)
122
+
123
+
124
+ def render_update_result(result: UpdateResult, json_output: bool) -> None:
125
+ if json_output:
126
+ print(json.dumps(result.model_dump(mode="json"), indent=2))
127
+ return
128
+ render_update_plan(result.plan, json_output=False)
129
+ if result.exit_code is not None:
130
+ print(f"exit_code: {result.exit_code}")
131
+ if result.stdout:
132
+ print(result.stdout, end="" if result.stdout.endswith("\n") else "\n")
133
+ if result.stderr:
134
+ print(result.stderr, end="" if result.stderr.endswith("\n") else "\n")
135
+
136
+
137
+ def shell_command(command: tuple[str, ...]) -> str:
138
+ return shlex.join(command)
139
+
140
+
141
+ def render_runs(records: tuple[RunRecord, ...], json_output: bool) -> None:
142
+ if json_output:
143
+ data = [record.model_dump(mode="json") for record in records]
144
+ print(json.dumps(data, indent=2))
145
+ return
146
+ if len(records) == 0:
147
+ print("# 0 jobs", file=sys.stderr)
148
+ return
149
+ for record in records:
150
+ title = record.title or ""
151
+ print(
152
+ f"{record.run_id}\t{record.status.value}\t"
153
+ f"{record.operation.value}\t{title}"
154
+ )
155
+
156
+
157
+ def render_run(record: RunRecord, json_output: bool) -> None:
158
+ if json_output:
159
+ print(json.dumps(record.model_dump(mode="json"), indent=2))
160
+ return
161
+ print(f"id: {record.run_id}")
162
+ print(f"operation: {record.operation.value}")
163
+ print(f"status: {record.status.value}")
164
+ if record.title is not None:
165
+ print(f"title: {record.title}")
166
+ if record.summary is not None:
167
+ print(f"summary: {record.summary}")
168
+ if record.error is not None:
169
+ print(f"error: {record.error}")
170
+ if record.harness_transcript is not None:
171
+ print(
172
+ "harness_transcript: "
173
+ f"{record.harness_transcript.kind.value} "
174
+ f"{record.harness_transcript.session_id}"
175
+ )
176
+ if record.harness_transcript.transcript_path is not None:
177
+ print(
178
+ "harness_transcript_path: "
179
+ f"{record.harness_transcript.transcript_path}"
180
+ )
181
+ print(f"created_at: {record.created_at.isoformat()}")
182
+ print(f"updated_at: {record.updated_at.isoformat()}")
183
+
184
+
185
+ def render_run_log(events: tuple[RunLogEvent, ...], json_output: bool) -> None:
186
+ if json_output:
187
+ data = [event.model_dump(mode="json") for event in events]
188
+ print(json.dumps(data, indent=2))
189
+ return
190
+ for event in events:
191
+ print(f"{event.sequence}\t{event.kind.value}\t{event.message}")