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.
- codealmanac/__init__.py +13 -0
- codealmanac/app.py +175 -0
- codealmanac/cli/__init__.py +1 -0
- codealmanac/cli/dispatch/__init__.py +0 -0
- codealmanac/cli/dispatch/admin.py +124 -0
- codealmanac/cli/dispatch/config.py +50 -0
- codealmanac/cli/dispatch/root.py +328 -0
- codealmanac/cli/main.py +28 -0
- codealmanac/cli/parser/__init__.py +0 -0
- codealmanac/cli/parser/admin.py +81 -0
- codealmanac/cli/parser/lifecycle.py +57 -0
- codealmanac/cli/parser/root.py +19 -0
- codealmanac/cli/parser/wiki.py +87 -0
- codealmanac/cli/render/__init__.py +0 -0
- codealmanac/cli/render/admin.py +191 -0
- codealmanac/cli/render/root.py +290 -0
- codealmanac/core/__init__.py +1 -0
- codealmanac/core/errors.py +45 -0
- codealmanac/core/models.py +14 -0
- codealmanac/core/paths.py +25 -0
- codealmanac/core/slug.py +7 -0
- codealmanac/core/text.py +5 -0
- codealmanac/database/__init__.py +15 -0
- codealmanac/database/sqlite.py +54 -0
- codealmanac/integrations/__init__.py +1 -0
- codealmanac/integrations/automation/__init__.py +3 -0
- codealmanac/integrations/automation/scheduler/__init__.py +5 -0
- codealmanac/integrations/automation/scheduler/launchd.py +163 -0
- codealmanac/integrations/command.py +56 -0
- codealmanac/integrations/harnesses/__init__.py +7 -0
- codealmanac/integrations/harnesses/claude/__init__.py +1 -0
- codealmanac/integrations/harnesses/claude/adapter.py +217 -0
- codealmanac/integrations/harnesses/codex/__init__.py +3 -0
- codealmanac/integrations/harnesses/codex/adapter.py +221 -0
- codealmanac/integrations/harnesses/git_status.py +49 -0
- codealmanac/integrations/sources/__init__.py +29 -0
- codealmanac/integrations/sources/filesystem/__init__.py +5 -0
- codealmanac/integrations/sources/filesystem/adapter.py +685 -0
- codealmanac/integrations/sources/filesystem/selection.py +209 -0
- codealmanac/integrations/sources/git/__init__.py +3 -0
- codealmanac/integrations/sources/git/adapter.py +132 -0
- codealmanac/integrations/sources/github/__init__.py +3 -0
- codealmanac/integrations/sources/github/adapter.py +413 -0
- codealmanac/integrations/sources/runtime.py +22 -0
- codealmanac/integrations/sources/transcripts/__init__.py +33 -0
- codealmanac/integrations/sources/transcripts/claude.py +61 -0
- codealmanac/integrations/sources/transcripts/codex.py +69 -0
- codealmanac/integrations/sources/transcripts/jsonl.py +84 -0
- codealmanac/integrations/sources/transcripts/runtime.py +387 -0
- codealmanac/integrations/sources/web/__init__.py +3 -0
- codealmanac/integrations/sources/web/adapter.py +303 -0
- codealmanac/integrations/updates/__init__.py +7 -0
- codealmanac/integrations/updates/package.py +85 -0
- codealmanac/integrations/workspaces/__init__.py +1 -0
- codealmanac/integrations/workspaces/git/__init__.py +3 -0
- codealmanac/integrations/workspaces/git/probe.py +128 -0
- codealmanac/manual/README.md +24 -0
- codealmanac/manual/__init__.py +19 -0
- codealmanac/manual/build.md +20 -0
- codealmanac/manual/evidence.md +23 -0
- codealmanac/manual/garden.md +20 -0
- codealmanac/manual/ingest.md +17 -0
- codealmanac/manual/library.py +84 -0
- codealmanac/manual/models.py +83 -0
- codealmanac/manual/pages.md +28 -0
- codealmanac/manual/requests.py +6 -0
- codealmanac/manual/sources.md +18 -0
- codealmanac/manual/style.md +19 -0
- codealmanac/prompts/__init__.py +5 -0
- codealmanac/prompts/base/notability.md +14 -0
- codealmanac/prompts/base/purpose.md +23 -0
- codealmanac/prompts/base/syntax.md +19 -0
- codealmanac/prompts/models.py +9 -0
- codealmanac/prompts/operations/garden.md +26 -0
- codealmanac/prompts/operations/ingest.md +18 -0
- codealmanac/prompts/renderer.py +24 -0
- codealmanac/prompts/requests.py +22 -0
- codealmanac/server/__init__.py +1 -0
- codealmanac/server/app.py +202 -0
- codealmanac/server/assets/__init__.py +1 -0
- codealmanac/server/assets/app.css +865 -0
- codealmanac/server/assets/app.js +3 -0
- codealmanac/server/assets/index.html +80 -0
- codealmanac/server/assets/viewer/api.js +30 -0
- codealmanac/server/assets/viewer/components.js +197 -0
- codealmanac/server/assets/viewer/main.js +126 -0
- codealmanac/server/assets/viewer/renderers.js +122 -0
- codealmanac/server/assets/viewer/routes.js +36 -0
- codealmanac/services/__init__.py +1 -0
- codealmanac/services/automation/__init__.py +3 -0
- codealmanac/services/automation/models.py +83 -0
- codealmanac/services/automation/ports.py +14 -0
- codealmanac/services/automation/requests.py +40 -0
- codealmanac/services/automation/service.py +294 -0
- codealmanac/services/config/__init__.py +17 -0
- codealmanac/services/config/models.py +61 -0
- codealmanac/services/config/requests.py +21 -0
- codealmanac/services/config/service.py +55 -0
- codealmanac/services/config/store.py +26 -0
- codealmanac/services/diagnostics/__init__.py +1 -0
- codealmanac/services/diagnostics/models.py +22 -0
- codealmanac/services/diagnostics/requests.py +8 -0
- codealmanac/services/diagnostics/service.py +283 -0
- codealmanac/services/harnesses/__init__.py +1 -0
- codealmanac/services/harnesses/models.py +104 -0
- codealmanac/services/harnesses/ports.py +18 -0
- codealmanac/services/harnesses/requests.py +19 -0
- codealmanac/services/harnesses/service.py +38 -0
- codealmanac/services/health/__init__.py +1 -0
- codealmanac/services/health/requests.py +8 -0
- codealmanac/services/health/service.py +20 -0
- codealmanac/services/index/__init__.py +1 -0
- codealmanac/services/index/models.py +135 -0
- codealmanac/services/index/requests.py +26 -0
- codealmanac/services/index/service.py +86 -0
- codealmanac/services/index/store.py +411 -0
- codealmanac/services/index/views.py +524 -0
- codealmanac/services/pages/__init__.py +1 -0
- codealmanac/services/pages/requests.py +17 -0
- codealmanac/services/pages/service.py +26 -0
- codealmanac/services/runs/__init__.py +1 -0
- codealmanac/services/runs/models.py +91 -0
- codealmanac/services/runs/requests.py +76 -0
- codealmanac/services/runs/service.py +86 -0
- codealmanac/services/runs/store.py +256 -0
- codealmanac/services/search/__init__.py +1 -0
- codealmanac/services/search/requests.py +23 -0
- codealmanac/services/search/service.py +31 -0
- codealmanac/services/sources/__init__.py +1 -0
- codealmanac/services/sources/models.py +126 -0
- codealmanac/services/sources/ports.py +30 -0
- codealmanac/services/sources/requests.py +76 -0
- codealmanac/services/sources/service.py +351 -0
- codealmanac/services/tagging/__init__.py +1 -0
- codealmanac/services/tagging/models.py +9 -0
- codealmanac/services/tagging/requests.py +35 -0
- codealmanac/services/tagging/service.py +43 -0
- codealmanac/services/topics/__init__.py +1 -0
- codealmanac/services/topics/models.py +36 -0
- codealmanac/services/topics/requests.py +115 -0
- codealmanac/services/topics/service.py +297 -0
- codealmanac/services/updates/__init__.py +4 -0
- codealmanac/services/updates/models.py +83 -0
- codealmanac/services/updates/ports.py +17 -0
- codealmanac/services/updates/requests.py +10 -0
- codealmanac/services/updates/service.py +113 -0
- codealmanac/services/viewer/__init__.py +1 -0
- codealmanac/services/viewer/models.py +80 -0
- codealmanac/services/viewer/renderer.py +89 -0
- codealmanac/services/viewer/requests.py +86 -0
- codealmanac/services/viewer/service.py +211 -0
- codealmanac/services/wiki/__init__.py +1 -0
- codealmanac/services/wiki/documents.py +83 -0
- codealmanac/services/wiki/frontmatter.py +94 -0
- codealmanac/services/wiki/frontmatter_rewrite.py +142 -0
- codealmanac/services/wiki/models.py +69 -0
- codealmanac/services/wiki/paths.py +42 -0
- codealmanac/services/wiki/service.py +57 -0
- codealmanac/services/wiki/templates.py +73 -0
- codealmanac/services/wiki/topics.py +266 -0
- codealmanac/services/wiki/wikilinks.py +58 -0
- codealmanac/services/workspaces/__init__.py +1 -0
- codealmanac/services/workspaces/models.py +124 -0
- codealmanac/services/workspaces/ports.py +9 -0
- codealmanac/services/workspaces/requests.py +82 -0
- codealmanac/services/workspaces/roots.py +74 -0
- codealmanac/services/workspaces/service.py +303 -0
- codealmanac/services/workspaces/store.py +127 -0
- codealmanac/workflows/__init__.py +1 -0
- codealmanac/workflows/build/__init__.py +1 -0
- codealmanac/workflows/build/models.py +8 -0
- codealmanac/workflows/build/service.py +45 -0
- codealmanac/workflows/garden/__init__.py +3 -0
- codealmanac/workflows/garden/models.py +30 -0
- codealmanac/workflows/garden/requests.py +22 -0
- codealmanac/workflows/garden/service.py +239 -0
- codealmanac/workflows/ingest/__init__.py +1 -0
- codealmanac/workflows/ingest/models.py +26 -0
- codealmanac/workflows/ingest/requests.py +39 -0
- codealmanac/workflows/ingest/service.py +302 -0
- codealmanac/workflows/lifecycle.py +197 -0
- codealmanac/workflows/sync/__init__.py +3 -0
- codealmanac/workflows/sync/models.py +157 -0
- codealmanac/workflows/sync/requests.py +63 -0
- codealmanac/workflows/sync/service.py +651 -0
- codealmanac/workflows/sync/store.py +51 -0
- codealmanac-0.1.0.dev0.dist-info/METADATA +248 -0
- codealmanac-0.1.0.dev0.dist-info/RECORD +192 -0
- codealmanac-0.1.0.dev0.dist-info/WHEEL +5 -0
- codealmanac-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- codealmanac-0.1.0.dev0.dist-info/licenses/LICENSE.md +201 -0
- codealmanac-0.1.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>CodeAlmanac</title>
|
|
7
|
+
<link rel="stylesheet" href="/app.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<a class="skip-link" href="#viewer-main">Skip to wiki</a>
|
|
11
|
+
<div class="dashboard-shell">
|
|
12
|
+
<aside class="dashboard-rail">
|
|
13
|
+
<div class="dashboard-rail-top">
|
|
14
|
+
<div class="dashboard-rail-picker">
|
|
15
|
+
<a class="dashboard-account-trigger" href="#/" aria-label="Current local wiki">
|
|
16
|
+
<span class="app-avatar app-avatar--account">
|
|
17
|
+
<span class="app-avatar-fallback">CA</span>
|
|
18
|
+
</span>
|
|
19
|
+
<span class="dashboard-account-text">
|
|
20
|
+
<span class="dashboard-account-name" id="workspace-name">Wiki</span>
|
|
21
|
+
<span class="dashboard-account-subtitle">repo-owned wiki</span>
|
|
22
|
+
</span>
|
|
23
|
+
<span class="dashboard-account-caret" aria-hidden="true">v</span>
|
|
24
|
+
</a>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="dashboard-rail-nav">
|
|
29
|
+
<p class="dashboard-nav-scope">Local knowledge graph</p>
|
|
30
|
+
<nav class="dashboard-nav" aria-label="Wiki navigation">
|
|
31
|
+
<a class="dashboard-nav-item is-active" data-nav-kind="home" href="#/">
|
|
32
|
+
<span class="nav-glyph">W</span>
|
|
33
|
+
Overview
|
|
34
|
+
</a>
|
|
35
|
+
<a class="dashboard-nav-item" data-nav-kind="search" href="#/search/">
|
|
36
|
+
<span class="nav-glyph">S</span>
|
|
37
|
+
Search
|
|
38
|
+
</a>
|
|
39
|
+
</nav>
|
|
40
|
+
|
|
41
|
+
<section class="wiki-rail-section">
|
|
42
|
+
<h2>Topics</h2>
|
|
43
|
+
<div id="topic-list" class="wiki-rail-list"></div>
|
|
44
|
+
</section>
|
|
45
|
+
|
|
46
|
+
<section class="wiki-rail-section">
|
|
47
|
+
<h2>Pages</h2>
|
|
48
|
+
<div id="page-list" class="wiki-rail-list"></div>
|
|
49
|
+
</section>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="dashboard-rail-user-desktop">
|
|
53
|
+
<div class="local-viewer-note">
|
|
54
|
+
<strong>Read-only local viewer</strong>
|
|
55
|
+
<span>Markdown stays in the repository. Use the CLI to change it.</span>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</aside>
|
|
59
|
+
|
|
60
|
+
<main id="viewer-main" class="dashboard-main">
|
|
61
|
+
<header class="dashboard-header">
|
|
62
|
+
<div class="dashboard-header-inner">
|
|
63
|
+
<div class="min-w-0">
|
|
64
|
+
<p class="dashboard-page-eyebrow">CodeAlmanac</p>
|
|
65
|
+
<h1 class="dashboard-header-title" id="route-title">Wiki</h1>
|
|
66
|
+
</div>
|
|
67
|
+
<form id="search-form" class="wiki-search-form">
|
|
68
|
+
<label class="sr-only" for="search-input">Search wiki</label>
|
|
69
|
+
<input id="search-input" name="q" type="search" placeholder="Search pages" />
|
|
70
|
+
<button class="wiki-search-button" type="submit">Search</button>
|
|
71
|
+
</form>
|
|
72
|
+
</div>
|
|
73
|
+
</header>
|
|
74
|
+
|
|
75
|
+
<div id="main" class="dashboard-page-body" tabindex="-1"></div>
|
|
76
|
+
</main>
|
|
77
|
+
</div>
|
|
78
|
+
<script type="module" src="/app.js"></script>
|
|
79
|
+
</body>
|
|
80
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const viewerApi = {
|
|
2
|
+
overview() {
|
|
3
|
+
return getJson("/api/overview");
|
|
4
|
+
},
|
|
5
|
+
|
|
6
|
+
page(slug) {
|
|
7
|
+
return getJson(`/api/page/${encodeURIComponent(slug)}`);
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
search(query) {
|
|
11
|
+
return getJson(`/api/search?q=${encodeURIComponent(query)}`);
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
topic(slug) {
|
|
15
|
+
return getJson(`/api/topic/${encodeURIComponent(slug)}`);
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
file(path) {
|
|
19
|
+
return getJson(`/api/file?path=${encodeURIComponent(path)}`);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
async function getJson(url) {
|
|
24
|
+
const response = await fetch(url);
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(data.detail?.message || `Request failed: ${response.status}`);
|
|
28
|
+
}
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { fileHref, homeHref, pageHref, topicHref } from "./routes.js";
|
|
2
|
+
|
|
3
|
+
export function pageIntro(eyebrow, title, copy) {
|
|
4
|
+
const section = document.createElement("section");
|
|
5
|
+
section.className = "dashboard-page-intro";
|
|
6
|
+
if (eyebrow) {
|
|
7
|
+
const label = document.createElement("p");
|
|
8
|
+
label.className = "dashboard-page-eyebrow";
|
|
9
|
+
label.textContent = eyebrow;
|
|
10
|
+
section.append(label);
|
|
11
|
+
}
|
|
12
|
+
const h1 = document.createElement("h1");
|
|
13
|
+
h1.className = "dashboard-page-title";
|
|
14
|
+
h1.textContent = title;
|
|
15
|
+
section.append(h1);
|
|
16
|
+
if (copy) {
|
|
17
|
+
const p = document.createElement("p");
|
|
18
|
+
p.className = "dashboard-page-copy";
|
|
19
|
+
p.textContent = copy;
|
|
20
|
+
section.append(p);
|
|
21
|
+
}
|
|
22
|
+
return section;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function statsGrid(overview) {
|
|
26
|
+
const grid = document.createElement("section");
|
|
27
|
+
grid.className = "wiki-stats";
|
|
28
|
+
grid.append(
|
|
29
|
+
statCard(String(overview.page_count), "Pages"),
|
|
30
|
+
statCard(String(overview.topic_count), "Topics"),
|
|
31
|
+
statCard(overview.workspace.name, "Current wiki"),
|
|
32
|
+
);
|
|
33
|
+
return grid;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function pageTitleBlock(page) {
|
|
37
|
+
const header = document.createElement("header");
|
|
38
|
+
header.className = "wiki-page-title-block";
|
|
39
|
+
|
|
40
|
+
const path = document.createElement("p");
|
|
41
|
+
path.className = "wiki-page-path";
|
|
42
|
+
path.textContent = `${page.slug}.md`;
|
|
43
|
+
|
|
44
|
+
const title = document.createElement("h1");
|
|
45
|
+
title.className = "wiki-page-title";
|
|
46
|
+
title.textContent = page.title || page.slug;
|
|
47
|
+
|
|
48
|
+
header.append(path, title);
|
|
49
|
+
if (page.summary) {
|
|
50
|
+
const summary = document.createElement("p");
|
|
51
|
+
summary.className = "wiki-page-summary";
|
|
52
|
+
summary.textContent = page.summary;
|
|
53
|
+
header.append(summary);
|
|
54
|
+
}
|
|
55
|
+
header.append(pageFacts(page));
|
|
56
|
+
return header;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function markdown(html) {
|
|
60
|
+
const article = document.createElement("article");
|
|
61
|
+
article.className = "wiki-markdown";
|
|
62
|
+
article.innerHTML = html;
|
|
63
|
+
return article;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function pageList(pages) {
|
|
67
|
+
if (pages.length === 0) {
|
|
68
|
+
return emptyState("No pages found", "Try a different search or topic.");
|
|
69
|
+
}
|
|
70
|
+
const list = document.createElement("nav");
|
|
71
|
+
list.className = "wiki-page-list";
|
|
72
|
+
list.setAttribute("aria-label", "Wiki pages");
|
|
73
|
+
for (const page of pages) {
|
|
74
|
+
list.append(pageRow(page));
|
|
75
|
+
}
|
|
76
|
+
return list;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function navLink(href, label, options = {}) {
|
|
80
|
+
const link = document.createElement("a");
|
|
81
|
+
link.href = href;
|
|
82
|
+
link.textContent = label;
|
|
83
|
+
if (options.kind) {
|
|
84
|
+
link.dataset.railKind = options.kind;
|
|
85
|
+
}
|
|
86
|
+
if (options.value) {
|
|
87
|
+
link.dataset.railValue = options.value;
|
|
88
|
+
}
|
|
89
|
+
if (options.title) {
|
|
90
|
+
link.title = options.title;
|
|
91
|
+
}
|
|
92
|
+
return link;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function sideSection(title, children) {
|
|
96
|
+
const section = document.createElement("section");
|
|
97
|
+
section.className = "wiki-side-section";
|
|
98
|
+
const heading = document.createElement("h2");
|
|
99
|
+
heading.textContent = title;
|
|
100
|
+
const list = document.createElement("div");
|
|
101
|
+
list.className = "wiki-link-list";
|
|
102
|
+
if (children.length === 0) {
|
|
103
|
+
list.append(sidebarEmpty());
|
|
104
|
+
} else {
|
|
105
|
+
list.append(...children);
|
|
106
|
+
}
|
|
107
|
+
section.append(heading, list);
|
|
108
|
+
return section;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function sideLink(href, label) {
|
|
112
|
+
const link = document.createElement("a");
|
|
113
|
+
link.href = href;
|
|
114
|
+
link.textContent = label;
|
|
115
|
+
return link;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function pageSideLink(slug, label) {
|
|
119
|
+
return sideLink(pageHref(slug), label);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function fileSideLink(path) {
|
|
123
|
+
return sideLink(fileHref(path), path);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function emptyState(title, body) {
|
|
127
|
+
const box = document.createElement("section");
|
|
128
|
+
box.className = "app-empty";
|
|
129
|
+
const h2 = document.createElement("h2");
|
|
130
|
+
h2.className = "app-empty-title";
|
|
131
|
+
h2.textContent = title;
|
|
132
|
+
const p = document.createElement("p");
|
|
133
|
+
p.className = "app-empty-body";
|
|
134
|
+
p.textContent = body;
|
|
135
|
+
box.append(h2, p);
|
|
136
|
+
return box;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function statCard(value, label) {
|
|
140
|
+
const card = document.createElement("div");
|
|
141
|
+
card.className = "wiki-stat";
|
|
142
|
+
const strong = document.createElement("strong");
|
|
143
|
+
strong.textContent = value;
|
|
144
|
+
const span = document.createElement("span");
|
|
145
|
+
span.textContent = label;
|
|
146
|
+
card.append(strong, span);
|
|
147
|
+
return card;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function pageFacts(page) {
|
|
151
|
+
const row = document.createElement("div");
|
|
152
|
+
row.className = "wiki-page-facts";
|
|
153
|
+
const allPages = document.createElement("a");
|
|
154
|
+
allPages.href = homeHref();
|
|
155
|
+
allPages.textContent = "All pages";
|
|
156
|
+
row.append(allPages);
|
|
157
|
+
for (const topic of page.topics) {
|
|
158
|
+
const link = document.createElement("a");
|
|
159
|
+
link.href = topicHref(topic);
|
|
160
|
+
link.textContent = topic;
|
|
161
|
+
row.append(link);
|
|
162
|
+
}
|
|
163
|
+
return row;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function pageRow(page) {
|
|
167
|
+
const item = document.createElement("a");
|
|
168
|
+
item.className = "wiki-page-row";
|
|
169
|
+
item.href = pageHref(page.slug);
|
|
170
|
+
|
|
171
|
+
const main = document.createElement("span");
|
|
172
|
+
main.className = "wiki-page-row-main";
|
|
173
|
+
const title = document.createElement("span");
|
|
174
|
+
title.className = "wiki-page-row-title";
|
|
175
|
+
title.textContent = page.title || page.slug;
|
|
176
|
+
const summary = document.createElement("span");
|
|
177
|
+
summary.className = "wiki-page-row-summary";
|
|
178
|
+
summary.textContent = page.summary || "No summary";
|
|
179
|
+
main.append(title, summary);
|
|
180
|
+
|
|
181
|
+
const meta = document.createElement("span");
|
|
182
|
+
meta.className = "wiki-page-row-meta";
|
|
183
|
+
const slug = document.createElement("span");
|
|
184
|
+
slug.textContent = `${page.slug}.md`;
|
|
185
|
+
const topics = document.createElement("span");
|
|
186
|
+
topics.textContent = page.topics.slice(0, 3).join(", ") || "untagged";
|
|
187
|
+
meta.append(slug, topics);
|
|
188
|
+
|
|
189
|
+
item.append(main, meta);
|
|
190
|
+
return item;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function sidebarEmpty() {
|
|
194
|
+
const empty = document.createElement("span");
|
|
195
|
+
empty.textContent = "None";
|
|
196
|
+
return empty;
|
|
197
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { viewerApi } from "./api.js";
|
|
2
|
+
import { navLink } from "./components.js";
|
|
3
|
+
import {
|
|
4
|
+
renderError,
|
|
5
|
+
renderFile,
|
|
6
|
+
renderHome,
|
|
7
|
+
renderPage,
|
|
8
|
+
renderSearch,
|
|
9
|
+
renderTopic,
|
|
10
|
+
} from "./renderers.js";
|
|
11
|
+
import {
|
|
12
|
+
pageHref,
|
|
13
|
+
parseHash,
|
|
14
|
+
RouteKind,
|
|
15
|
+
searchHref,
|
|
16
|
+
topicHref,
|
|
17
|
+
} from "./routes.js";
|
|
18
|
+
|
|
19
|
+
const state = {
|
|
20
|
+
overview: null,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function startViewer() {
|
|
24
|
+
const elements = readElements();
|
|
25
|
+
elements.searchForm.addEventListener("submit", (event) => {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
window.location.hash = searchHref(elements.searchInput.value.trim());
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
window.addEventListener("hashchange", () => route(elements));
|
|
31
|
+
loadOverview(elements).then(() => route(elements));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function loadOverview(elements) {
|
|
35
|
+
state.overview = await viewerApi.overview();
|
|
36
|
+
elements.workspaceName.textContent = state.overview.workspace.name;
|
|
37
|
+
renderNav(elements, state.overview);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function route(elements) {
|
|
41
|
+
if (!state.overview) return;
|
|
42
|
+
const context = {
|
|
43
|
+
elements,
|
|
44
|
+
overview: state.overview,
|
|
45
|
+
setRouteTitle: (title) => setRouteTitle(elements, title),
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
const routeState = parseHash(window.location.hash);
|
|
49
|
+
setActiveNav(elements, routeState);
|
|
50
|
+
if (routeState.kind === RouteKind.PAGE && routeState.value) {
|
|
51
|
+
await renderPage(context, routeState.value);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (routeState.kind === RouteKind.TOPIC && routeState.value) {
|
|
55
|
+
await renderTopic(context, routeState.value);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (routeState.kind === RouteKind.SEARCH) {
|
|
59
|
+
await renderSearch(context, routeState.value);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (routeState.kind === RouteKind.FILE && routeState.value) {
|
|
63
|
+
await renderFile(context, routeState.value);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
renderHome(context);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
renderError(context, error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readElements() {
|
|
73
|
+
return {
|
|
74
|
+
workspaceName: document.getElementById("workspace-name"),
|
|
75
|
+
routeTitle: document.getElementById("route-title"),
|
|
76
|
+
searchForm: document.getElementById("search-form"),
|
|
77
|
+
searchInput: document.getElementById("search-input"),
|
|
78
|
+
topicList: document.getElementById("topic-list"),
|
|
79
|
+
pageList: document.getElementById("page-list"),
|
|
80
|
+
main: document.getElementById("main"),
|
|
81
|
+
navItems: Array.from(document.querySelectorAll("[data-nav-kind]")),
|
|
82
|
+
railLinks: () => Array.from(document.querySelectorAll("[data-rail-kind]")),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function renderNav(elements, overview) {
|
|
87
|
+
elements.topicList.replaceChildren(
|
|
88
|
+
...overview.topics.map((topic) =>
|
|
89
|
+
navLink(topicHref(topic.slug), topic.title || topic.slug, {
|
|
90
|
+
kind: RouteKind.TOPIC,
|
|
91
|
+
value: topic.slug,
|
|
92
|
+
title: `${topic.page_count} pages`,
|
|
93
|
+
}),
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
elements.pageList.replaceChildren(
|
|
97
|
+
...overview.pages.map((page) =>
|
|
98
|
+
navLink(pageHref(page.slug), page.title || page.slug, {
|
|
99
|
+
kind: RouteKind.PAGE,
|
|
100
|
+
value: page.slug,
|
|
101
|
+
title: page.summary || `${page.slug}.md`,
|
|
102
|
+
}),
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function setActiveNav(elements, routeState) {
|
|
108
|
+
const active =
|
|
109
|
+
routeState.kind === RouteKind.HOME || routeState.kind === RouteKind.SEARCH
|
|
110
|
+
? routeState.kind
|
|
111
|
+
: "";
|
|
112
|
+
for (const item of elements.navItems) {
|
|
113
|
+
item.classList.toggle("is-active", item.dataset.navKind === active);
|
|
114
|
+
}
|
|
115
|
+
for (const link of elements.railLinks()) {
|
|
116
|
+
const routeMatches =
|
|
117
|
+
link.dataset.railKind === routeState.kind &&
|
|
118
|
+
link.dataset.railValue === routeState.value;
|
|
119
|
+
link.classList.toggle("is-active", routeMatches);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function setRouteTitle(elements, title) {
|
|
124
|
+
document.title = `${title} | CodeAlmanac`;
|
|
125
|
+
elements.routeTitle.textContent = title;
|
|
126
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { viewerApi } from "./api.js";
|
|
2
|
+
import {
|
|
3
|
+
emptyState,
|
|
4
|
+
fileSideLink,
|
|
5
|
+
markdown,
|
|
6
|
+
pageIntro,
|
|
7
|
+
pageList,
|
|
8
|
+
pageSideLink,
|
|
9
|
+
pageTitleBlock,
|
|
10
|
+
sideSection,
|
|
11
|
+
statsGrid,
|
|
12
|
+
} from "./components.js";
|
|
13
|
+
|
|
14
|
+
export function renderHome(context) {
|
|
15
|
+
const { elements, overview, setRouteTitle } = context;
|
|
16
|
+
const title = overview.featured_page?.title || "Wiki";
|
|
17
|
+
setRouteTitle(title);
|
|
18
|
+
elements.searchInput.value = "";
|
|
19
|
+
replaceMain(
|
|
20
|
+
elements,
|
|
21
|
+
pageIntro(
|
|
22
|
+
"Local wiki",
|
|
23
|
+
title,
|
|
24
|
+
`${overview.page_count} pages across ${overview.topic_count} topics.`,
|
|
25
|
+
),
|
|
26
|
+
statsGrid(overview),
|
|
27
|
+
pageList(overview.pages),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function renderPage(context, slug) {
|
|
32
|
+
const { elements, setRouteTitle } = context;
|
|
33
|
+
const page = await viewerApi.page(slug);
|
|
34
|
+
setRouteTitle(page.title || page.slug);
|
|
35
|
+
|
|
36
|
+
const grid = document.createElement("div");
|
|
37
|
+
grid.className = "wiki-detail-grid";
|
|
38
|
+
|
|
39
|
+
const article = document.createElement("article");
|
|
40
|
+
article.className = "wiki-page-card";
|
|
41
|
+
article.append(pageTitleBlock(page), markdown(page.html));
|
|
42
|
+
|
|
43
|
+
const sidePanel = document.createElement("aside");
|
|
44
|
+
sidePanel.className = "wiki-side-panel";
|
|
45
|
+
sidePanel.setAttribute("aria-label", "Page context");
|
|
46
|
+
sidePanel.append(
|
|
47
|
+
sideSection("Backlinks", page.backlinks.map((item) => pageSideLink(item, item))),
|
|
48
|
+
sideSection("Files", page.file_refs.map((file) => fileSideLink(file.path))),
|
|
49
|
+
sideSection(
|
|
50
|
+
"Related",
|
|
51
|
+
page.related_pages.map((related) =>
|
|
52
|
+
pageSideLink(related.slug, related.title || related.slug),
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
grid.append(article, sidePanel);
|
|
58
|
+
replaceMain(elements, grid);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function renderTopic(context, slug) {
|
|
62
|
+
const { elements, setRouteTitle } = context;
|
|
63
|
+
const topic = await viewerApi.topic(slug);
|
|
64
|
+
setRouteTitle(topic.title || topic.slug);
|
|
65
|
+
replaceMain(
|
|
66
|
+
elements,
|
|
67
|
+
pageIntro("Topic", topic.title || topic.slug, topic.description || ""),
|
|
68
|
+
pageList(topic.pages),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function renderSearch(context, query) {
|
|
73
|
+
const { elements, setRouteTitle } = context;
|
|
74
|
+
elements.searchInput.value = query;
|
|
75
|
+
setRouteTitle(query ? `Search: ${query}` : "Search");
|
|
76
|
+
if (!query) {
|
|
77
|
+
replaceMain(
|
|
78
|
+
elements,
|
|
79
|
+
emptyState(
|
|
80
|
+
"Search the wiki",
|
|
81
|
+
"Search page titles, summaries, and page bodies in the local SQLite index.",
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const result = await viewerApi.search(query);
|
|
87
|
+
replaceMain(
|
|
88
|
+
elements,
|
|
89
|
+
pageIntro("Search", query, `${result.pages.length} results`),
|
|
90
|
+
pageList(result.pages),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function renderFile(context, path) {
|
|
95
|
+
const { elements, setRouteTitle } = context;
|
|
96
|
+
const result = await viewerApi.file(path);
|
|
97
|
+
const noun = result.kind === "directory" ? "folder" : "file";
|
|
98
|
+
setRouteTitle(result.path);
|
|
99
|
+
replaceMain(
|
|
100
|
+
elements,
|
|
101
|
+
pageIntro(
|
|
102
|
+
`Referenced ${noun}`,
|
|
103
|
+
result.path,
|
|
104
|
+
`${result.pages.length} pages mention this ${noun}.`,
|
|
105
|
+
),
|
|
106
|
+
pageList(result.pages),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function renderError(context, error) {
|
|
111
|
+
const { elements, setRouteTitle } = context;
|
|
112
|
+
setRouteTitle("Error");
|
|
113
|
+
const message = document.createElement("p");
|
|
114
|
+
message.className = "error";
|
|
115
|
+
message.textContent = error.message;
|
|
116
|
+
replaceMain(elements, pageIntro("Viewer error", "Error", ""), message);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function replaceMain(elements, ...children) {
|
|
120
|
+
elements.main.replaceChildren(...children);
|
|
121
|
+
elements.main.focus();
|
|
122
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const RouteKind = Object.freeze({
|
|
2
|
+
HOME: "home",
|
|
3
|
+
PAGE: "page",
|
|
4
|
+
TOPIC: "topic",
|
|
5
|
+
SEARCH: "search",
|
|
6
|
+
FILE: "file",
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export function parseHash(hash) {
|
|
10
|
+
const raw = hash.replace(/^#\/?/, "");
|
|
11
|
+
if (!raw) return { kind: RouteKind.HOME, value: "" };
|
|
12
|
+
const parts = raw.split("/");
|
|
13
|
+
const kind = parts[0];
|
|
14
|
+
const value = decodeURIComponent(parts.slice(1).join("/"));
|
|
15
|
+
return { kind, value };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function homeHref() {
|
|
19
|
+
return "#/";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function pageHref(slug) {
|
|
23
|
+
return `#/page/${encodeURIComponent(slug)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function topicHref(slug) {
|
|
27
|
+
return `#/topic/${encodeURIComponent(slug)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function searchHref(query) {
|
|
31
|
+
return query ? `#/search/${encodeURIComponent(query)}` : homeHref();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function fileHref(path) {
|
|
35
|
+
return `#/file/${encodeURIComponent(path)}`;
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from pydantic import field_validator
|
|
6
|
+
|
|
7
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
8
|
+
from codealmanac.core.text import required_text
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AutomationTask(StrEnum):
|
|
12
|
+
SYNC = "sync"
|
|
13
|
+
GARDEN = "garden"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AutomationWorkingDirectory(StrEnum):
|
|
17
|
+
NONE = "none"
|
|
18
|
+
CURRENT_WIKI = "current_wiki"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EnvironmentVariable(CodeAlmanacModel):
|
|
22
|
+
name: str
|
|
23
|
+
value: str
|
|
24
|
+
|
|
25
|
+
@field_validator("name")
|
|
26
|
+
@classmethod
|
|
27
|
+
def require_name(cls, value: str) -> str:
|
|
28
|
+
return required_text(value, "environment variable name")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ScheduledJob(CodeAlmanacModel):
|
|
32
|
+
task: AutomationTask
|
|
33
|
+
label: str
|
|
34
|
+
plist_path: Path
|
|
35
|
+
program_arguments: tuple[str, ...]
|
|
36
|
+
interval: timedelta
|
|
37
|
+
environment: tuple[EnvironmentVariable, ...]
|
|
38
|
+
stdout_path: Path
|
|
39
|
+
stderr_path: Path
|
|
40
|
+
working_directory: Path | None = None
|
|
41
|
+
|
|
42
|
+
@field_validator("label")
|
|
43
|
+
@classmethod
|
|
44
|
+
def require_label(cls, value: str) -> str:
|
|
45
|
+
return required_text(value, "scheduled job label")
|
|
46
|
+
|
|
47
|
+
@field_validator("program_arguments")
|
|
48
|
+
@classmethod
|
|
49
|
+
def require_program_arguments(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
50
|
+
if len(value) == 0:
|
|
51
|
+
raise ValueError("scheduled job program arguments are required")
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
@field_validator("interval")
|
|
55
|
+
@classmethod
|
|
56
|
+
def positive_interval(cls, value: timedelta) -> timedelta:
|
|
57
|
+
if value.total_seconds() <= 0:
|
|
58
|
+
raise ValueError("scheduled job interval must be greater than zero")
|
|
59
|
+
return value
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ScheduledJobStatus(CodeAlmanacModel):
|
|
63
|
+
task: AutomationTask
|
|
64
|
+
label: str
|
|
65
|
+
plist_path: Path
|
|
66
|
+
installed: bool
|
|
67
|
+
loaded: bool
|
|
68
|
+
interval: timedelta | None = None
|
|
69
|
+
quiet: timedelta | None = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AutomationInstallResult(CodeAlmanacModel):
|
|
73
|
+
jobs: tuple[ScheduledJob, ...]
|
|
74
|
+
disabled: tuple[ScheduledJob, ...] = ()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AutomationUninstallResult(CodeAlmanacModel):
|
|
78
|
+
tasks: tuple[AutomationTask, ...]
|
|
79
|
+
removed: tuple[Path, ...]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AutomationStatusReport(CodeAlmanacModel):
|
|
83
|
+
statuses: tuple[ScheduledJobStatus, ...]
|