@mfittko/repo-wiki 0.2.1

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 (190) hide show
  1. package/.llmwiki/schema.md +107 -0
  2. package/AGENTS.md +42 -0
  3. package/CHANGELOG.md +91 -0
  4. package/LICENSE +21 -0
  5. package/README.md +254 -0
  6. package/dist/bin/repo-wiki.d.ts +2 -0
  7. package/dist/bin/repo-wiki.js +7 -0
  8. package/dist/bin/repo-wiki.js.map +1 -0
  9. package/dist/src/cli.d.ts +1 -0
  10. package/dist/src/cli.js +404 -0
  11. package/dist/src/cli.js.map +1 -0
  12. package/dist/src/compiler.d.ts +55 -0
  13. package/dist/src/compiler.js +2046 -0
  14. package/dist/src/compiler.js.map +1 -0
  15. package/dist/src/config.d.ts +63 -0
  16. package/dist/src/config.js +86 -0
  17. package/dist/src/config.js.map +1 -0
  18. package/dist/src/context-assembler.d.ts +68 -0
  19. package/dist/src/context-assembler.js +378 -0
  20. package/dist/src/context-assembler.js.map +1 -0
  21. package/dist/src/data-model-signals.d.ts +1 -0
  22. package/dist/src/data-model-signals.js +13 -0
  23. package/dist/src/data-model-signals.js.map +1 -0
  24. package/dist/src/docs-ingestor.d.ts +138 -0
  25. package/dist/src/docs-ingestor.js +844 -0
  26. package/dist/src/docs-ingestor.js.map +1 -0
  27. package/dist/src/docs-linter.d.ts +14 -0
  28. package/dist/src/docs-linter.js +164 -0
  29. package/dist/src/docs-linter.js.map +1 -0
  30. package/dist/src/docs-validation.d.ts +36 -0
  31. package/dist/src/docs-validation.js +297 -0
  32. package/dist/src/docs-validation.js.map +1 -0
  33. package/dist/src/extractors.d.ts +50 -0
  34. package/dist/src/extractors.js +2275 -0
  35. package/dist/src/extractors.js.map +1 -0
  36. package/dist/src/frontmatter.d.ts +46 -0
  37. package/dist/src/frontmatter.js +377 -0
  38. package/dist/src/frontmatter.js.map +1 -0
  39. package/dist/src/index.d.ts +26 -0
  40. package/dist/src/index.js +18 -0
  41. package/dist/src/index.js.map +1 -0
  42. package/dist/src/init.d.ts +12 -0
  43. package/dist/src/init.js +121 -0
  44. package/dist/src/init.js.map +1 -0
  45. package/dist/src/language.d.ts +2 -0
  46. package/dist/src/language.js +62 -0
  47. package/dist/src/language.js.map +1 -0
  48. package/dist/src/linter.d.ts +33 -0
  49. package/dist/src/linter.js +398 -0
  50. package/dist/src/linter.js.map +1 -0
  51. package/dist/src/llm-provider.d.ts +267 -0
  52. package/dist/src/llm-provider.js +474 -0
  53. package/dist/src/llm-provider.js.map +1 -0
  54. package/dist/src/page-ownership.d.ts +38 -0
  55. package/dist/src/page-ownership.js +96 -0
  56. package/dist/src/page-ownership.js.map +1 -0
  57. package/dist/src/planner.d.ts +55 -0
  58. package/dist/src/planner.js +422 -0
  59. package/dist/src/planner.js.map +1 -0
  60. package/dist/src/prompts.d.ts +103 -0
  61. package/dist/src/prompts.js +344 -0
  62. package/dist/src/prompts.js.map +1 -0
  63. package/dist/src/publisher.d.ts +68 -0
  64. package/dist/src/publisher.js +662 -0
  65. package/dist/src/publisher.js.map +1 -0
  66. package/dist/src/repository-analysis.d.ts +88 -0
  67. package/dist/src/repository-analysis.js +485 -0
  68. package/dist/src/repository-analysis.js.map +1 -0
  69. package/dist/src/scanner.d.ts +122 -0
  70. package/dist/src/scanner.js +309 -0
  71. package/dist/src/scanner.js.map +1 -0
  72. package/dist/src/search.d.ts +71 -0
  73. package/dist/src/search.js +410 -0
  74. package/dist/src/search.js.map +1 -0
  75. package/dist/src/secret-patterns.d.ts +3 -0
  76. package/dist/src/secret-patterns.js +14 -0
  77. package/dist/src/secret-patterns.js.map +1 -0
  78. package/dist/src/utils/args.d.ts +2 -0
  79. package/dist/src/utils/args.js +19 -0
  80. package/dist/src/utils/args.js.map +1 -0
  81. package/dist/src/utils/dotenv.d.ts +7 -0
  82. package/dist/src/utils/dotenv.js +73 -0
  83. package/dist/src/utils/dotenv.js.map +1 -0
  84. package/dist/src/utils/fs.d.ts +22 -0
  85. package/dist/src/utils/fs.js +83 -0
  86. package/dist/src/utils/fs.js.map +1 -0
  87. package/dist/src/utils/git.d.ts +13 -0
  88. package/dist/src/utils/git.js +39 -0
  89. package/dist/src/utils/git.js.map +1 -0
  90. package/dist/src/wiki-graph.d.ts +74 -0
  91. package/dist/src/wiki-graph.js +335 -0
  92. package/dist/src/wiki-graph.js.map +1 -0
  93. package/dist/src/wiki-patch.d.ts +152 -0
  94. package/dist/src/wiki-patch.js +489 -0
  95. package/dist/src/wiki-patch.js.map +1 -0
  96. package/dist/src/wiki-query.d.ts +63 -0
  97. package/dist/src/wiki-query.js +255 -0
  98. package/dist/src/wiki-query.js.map +1 -0
  99. package/dist/test/cli.test.d.ts +1 -0
  100. package/dist/test/cli.test.js +514 -0
  101. package/dist/test/cli.test.js.map +1 -0
  102. package/dist/test/compiler-eval.test.d.ts +1 -0
  103. package/dist/test/compiler-eval.test.js +234 -0
  104. package/dist/test/compiler-eval.test.js.map +1 -0
  105. package/dist/test/compiler.test.d.ts +1 -0
  106. package/dist/test/compiler.test.js +2537 -0
  107. package/dist/test/compiler.test.js.map +1 -0
  108. package/dist/test/context-assembler.test.d.ts +1 -0
  109. package/dist/test/context-assembler.test.js +379 -0
  110. package/dist/test/context-assembler.test.js.map +1 -0
  111. package/dist/test/docs-linter.test.d.ts +1 -0
  112. package/dist/test/docs-linter.test.js +900 -0
  113. package/dist/test/docs-linter.test.js.map +1 -0
  114. package/dist/test/dotenv.test.d.ts +1 -0
  115. package/dist/test/dotenv.test.js +77 -0
  116. package/dist/test/dotenv.test.js.map +1 -0
  117. package/dist/test/extractors-go.test.d.ts +1 -0
  118. package/dist/test/extractors-go.test.js +393 -0
  119. package/dist/test/extractors-go.test.js.map +1 -0
  120. package/dist/test/extractors-rust.test.d.ts +1 -0
  121. package/dist/test/extractors-rust.test.js +219 -0
  122. package/dist/test/extractors-rust.test.js.map +1 -0
  123. package/dist/test/extractors-utils.test.d.ts +1 -0
  124. package/dist/test/extractors-utils.test.js +786 -0
  125. package/dist/test/extractors-utils.test.js.map +1 -0
  126. package/dist/test/fixtures/compiler-e2e/basic-node-service/repo/infra/deploy.d.ts +1 -0
  127. package/dist/test/fixtures/compiler-e2e/basic-node-service/repo/infra/deploy.js +4 -0
  128. package/dist/test/fixtures/compiler-e2e/basic-node-service/repo/infra/deploy.js.map +1 -0
  129. package/dist/test/frontmatter.test.d.ts +1 -0
  130. package/dist/test/frontmatter.test.js +287 -0
  131. package/dist/test/frontmatter.test.js.map +1 -0
  132. package/dist/test/init-planner.test.d.ts +1 -0
  133. package/dist/test/init-planner.test.js +688 -0
  134. package/dist/test/init-planner.test.js.map +1 -0
  135. package/dist/test/linter.test.d.ts +1 -0
  136. package/dist/test/linter.test.js +426 -0
  137. package/dist/test/linter.test.js.map +1 -0
  138. package/dist/test/llm-provider.test.d.ts +1 -0
  139. package/dist/test/llm-provider.test.js +783 -0
  140. package/dist/test/llm-provider.test.js.map +1 -0
  141. package/dist/test/page-ownership.test.d.ts +1 -0
  142. package/dist/test/page-ownership.test.js +247 -0
  143. package/dist/test/page-ownership.test.js.map +1 -0
  144. package/dist/test/publisher.test.d.ts +1 -0
  145. package/dist/test/publisher.test.js +1297 -0
  146. package/dist/test/publisher.test.js.map +1 -0
  147. package/dist/test/repository-analysis.test.d.ts +1 -0
  148. package/dist/test/repository-analysis.test.js +182 -0
  149. package/dist/test/repository-analysis.test.js.map +1 -0
  150. package/dist/test/run-compiled-tests.d.ts +1 -0
  151. package/dist/test/run-compiled-tests.js +48 -0
  152. package/dist/test/run-compiled-tests.js.map +1 -0
  153. package/dist/test/scanner.test.d.ts +1 -0
  154. package/dist/test/scanner.test.js +551 -0
  155. package/dist/test/scanner.test.js.map +1 -0
  156. package/dist/test/search.test.d.ts +1 -0
  157. package/dist/test/search.test.js +92 -0
  158. package/dist/test/search.test.js.map +1 -0
  159. package/dist/test/update-changelog.test.d.ts +1 -0
  160. package/dist/test/update-changelog.test.js +125 -0
  161. package/dist/test/update-changelog.test.js.map +1 -0
  162. package/dist/test/wiki-graph.test.d.ts +1 -0
  163. package/dist/test/wiki-graph.test.js +164 -0
  164. package/dist/test/wiki-graph.test.js.map +1 -0
  165. package/dist/test/wiki-patch.test.d.ts +1 -0
  166. package/dist/test/wiki-patch.test.js +610 -0
  167. package/dist/test/wiki-patch.test.js.map +1 -0
  168. package/dist/test/wiki-query.test.d.ts +1 -0
  169. package/dist/test/wiki-query.test.js +163 -0
  170. package/dist/test/wiki-query.test.js.map +1 -0
  171. package/docs/PLAN.md +993 -0
  172. package/docs/WHY.md +61 -0
  173. package/docs/plans/agent-integration.md +85 -0
  174. package/docs/plans/ci-publishing.md +111 -0
  175. package/docs/plans/doc-validation.md +92 -0
  176. package/docs/plans/github-action.md +113 -0
  177. package/docs/plans/incremental-mode.md +98 -0
  178. package/docs/plans/karpathy-llm-wiki-alignment.md +84 -0
  179. package/docs/plans/llm-compiler.md +160 -0
  180. package/docs/plans/production-scanner.md +104 -0
  181. package/docs/plans/query-and-file-back.md +103 -0
  182. package/docs/plans/search-index.md +118 -0
  183. package/docs/plans/trust-hardening.md +74 -0
  184. package/docs/plans/wiki-graph.md +183 -0
  185. package/docs/plans/wiki-health.md +76 -0
  186. package/package.json +83 -0
  187. package/prompts/compiler.md +16 -0
  188. package/prompts/lint.md +18 -0
  189. package/prompts/page-templates.md +25 -0
  190. package/skills/repo-wiki-cli/SKILL.md +139 -0
@@ -0,0 +1,662 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileExists } from './utils/fs.js';
5
+ import { getGitStatus, runGit } from './utils/git.js';
6
+ import { applyFrontmatterPolicy, stripFrontmatter } from './frontmatter.js';
7
+ export const PUBLISH_TARGETS = ['github-wiki', 'github-pages'];
8
+ /**
9
+ * Marker embedded in every support file generated by repo-wiki for GitHub Pages.
10
+ * Files that contain this marker are regenerated on each publish so they stay
11
+ * up-to-date with new layout / navigation features. Files without this marker
12
+ * are treated as user-customized and are never overwritten.
13
+ */
14
+ const REPO_WIKI_GENERATED_MARKER = 'repo-wiki-generated: regenerated on each publish';
15
+ const REPO_WIKI_GENERATED_HTML_COMMENT = `<!-- ${REPO_WIKI_GENERATED_MARKER} -->`;
16
+ const REPO_WIKI_GENERATED_YAML_COMMENT = `# ${REPO_WIKI_GENERATED_MARKER}`;
17
+ /** Returns true if the file exists and was written by repo-wiki. */
18
+ async function isRepoWikiGeneratedFile(filePath, kind) {
19
+ try {
20
+ const content = await fs.readFile(filePath, 'utf8');
21
+ return isRepoWikiGeneratedContent(content, kind);
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ function isRepoWikiGeneratedContent(content, kind) {
28
+ if (content.includes(REPO_WIKI_GENERATED_MARKER)) {
29
+ return true;
30
+ }
31
+ switch (kind) {
32
+ case 'config':
33
+ return isLegacyRepoWikiPagesConfig(content);
34
+ case 'layout':
35
+ return isLegacyRepoWikiPagesLayout(content);
36
+ default:
37
+ return false;
38
+ }
39
+ }
40
+ function isLegacyRepoWikiPagesConfig(content) {
41
+ return /^defaults:\n - scope:\n path: ""\n values:\n layout: "repo-wiki"\n(?:wiki_pages_dir: "[^"\n]*"\n)?$/.test(content);
42
+ }
43
+ function isLegacyRepoWikiPagesLayout(content) {
44
+ return content.startsWith('<!doctype html>\n<html lang="en">\n')
45
+ && content.includes('<title>{% if page.title %}{{ page.title | escape }}{% else %}{{ page.name | replace: \'.md\', \'\' | escape }}{% endif %}')
46
+ && content.includes('import mermaid from \'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs\';')
47
+ && content.includes('document.querySelectorAll(\'pre > code.language-mermaid\')')
48
+ && (content.includes('<main>\n {{ content }}\n </main>')
49
+ || content.includes('{% assign _rp = page.path %}') && content.includes('{% include wiki_nav.html %}'));
50
+ }
51
+ export async function publishWiki({ wikiDir, remote, branch, target = 'github-wiki', pagesPath = '.', message, dryRun = false, frontmatterPolicy, gitUserName = process.env.LLMWIKI_GIT_USER_NAME || 'repo-wiki-bot', gitUserEmail = process.env.LLMWIKI_GIT_USER_EMAIL || 'repo-wiki-bot@users.noreply.github.com' }) {
52
+ const absoluteWikiDir = path.resolve(wikiDir || '.llmwiki/wiki');
53
+ const publishTarget = target;
54
+ const publishRemote = resolvePublishRemote(publishTarget, remote);
55
+ const summaryRemote = sanitizeRemote(publishRemote);
56
+ const publishBranch = branch || defaultBranchForTarget(publishTarget);
57
+ const publishFrontmatterPolicy = frontmatterPolicy || defaultFrontmatterPolicyForTarget(publishTarget);
58
+ const resolvedPublishPath = resolvePublishPath(publishTarget, pagesPath);
59
+ assertSafeGitArgument(publishBranch, 'branch');
60
+ assertSafeGitArgument(publishRemote, 'remote');
61
+ if (!await fileExists(absoluteWikiDir)) {
62
+ throw new Error(`Wiki directory does not exist: ${absoluteWikiDir}`);
63
+ }
64
+ const markdownFileCount = await countMarkdownFiles(absoluteWikiDir);
65
+ if (dryRun) {
66
+ return {
67
+ summary: {
68
+ status: 'dry-run',
69
+ wikiDir: absoluteWikiDir,
70
+ remote: summaryRemote,
71
+ branch: publishBranch,
72
+ target: publishTarget,
73
+ path: resolvedPublishPath.relative,
74
+ pages: markdownFileCount,
75
+ frontmatterPolicy: publishFrontmatterPolicy
76
+ }
77
+ };
78
+ }
79
+ if (!publishRemote) {
80
+ return {
81
+ summary: {
82
+ status: 'skipped-no-remote',
83
+ wikiDir: absoluteWikiDir,
84
+ remote: null,
85
+ branch: publishBranch,
86
+ target: publishTarget,
87
+ path: resolvedPublishPath.relative,
88
+ pages: markdownFileCount,
89
+ frontmatterPolicy: publishFrontmatterPolicy,
90
+ next_step: publishTarget === 'github-pages'
91
+ ? 'Set LLMWIKI_PUBLISH_REMOTE or pass --remote with a target repository URL, for example OWNER/REPO.git.'
92
+ : 'Set LLMWIKI_PUBLISH_REMOTE, GITHUB_WIKI_REMOTE, or pass --remote with an OWNER/REPO.wiki.git URL.'
93
+ }
94
+ };
95
+ }
96
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'repo-wiki-publish-'));
97
+ const checkoutDir = path.join(tempRoot, 'wiki');
98
+ const commitMessage = message || `Compile repository wiki ${new Date().toISOString()}`;
99
+ let cloned = false;
100
+ try {
101
+ try {
102
+ await runGit(['clone', '--branch', publishBranch, '--', publishRemote, checkoutDir]);
103
+ cloned = true;
104
+ }
105
+ catch (error) {
106
+ if (!isCloneFallbackError(error)) {
107
+ throw error;
108
+ }
109
+ await fs.mkdir(checkoutDir, { recursive: true });
110
+ await runGit(['init'], { cwd: checkoutDir });
111
+ await runGit(['remote', 'add', 'origin', publishRemote], { cwd: checkoutDir });
112
+ }
113
+ const publishDir = resolvedPublishPath.absoluteResolver(checkoutDir);
114
+ assertPublishPathContained(checkoutDir, publishDir);
115
+ if (publishTarget === 'github-pages') {
116
+ await cleanGeneratedMarkdown(publishDir);
117
+ }
118
+ else {
119
+ await cleanPublishPath(checkoutDir, publishDir);
120
+ }
121
+ await copyGeneratedWiki(absoluteWikiDir, publishDir, publishFrontmatterPolicy, publishTarget === 'github-pages');
122
+ if (publishTarget === 'github-pages') {
123
+ await ensurePagesSiteSupport(checkoutDir, publishDir, resolvedPublishPath.relative);
124
+ }
125
+ await runGit(['config', 'user.name', gitUserName], { cwd: checkoutDir });
126
+ await runGit(['config', 'user.email', gitUserEmail], { cwd: checkoutDir });
127
+ await runGit(['add', '.'], { cwd: checkoutDir });
128
+ const status = await getGitStatus(checkoutDir);
129
+ if (!status) {
130
+ return {
131
+ summary: {
132
+ status: 'no-changes',
133
+ wikiDir: absoluteWikiDir,
134
+ remote: summaryRemote,
135
+ branch: publishBranch,
136
+ target: publishTarget,
137
+ path: resolvedPublishPath.relative,
138
+ pages: markdownFileCount,
139
+ frontmatterPolicy: publishFrontmatterPolicy,
140
+ cloned
141
+ }
142
+ };
143
+ }
144
+ await runGit(['commit', '-m', commitMessage], { cwd: checkoutDir });
145
+ await runGit(['push', 'origin', `HEAD:${publishBranch}`], { cwd: checkoutDir });
146
+ return {
147
+ summary: {
148
+ status: 'published',
149
+ wikiDir: absoluteWikiDir,
150
+ remote: summaryRemote,
151
+ branch: publishBranch,
152
+ target: publishTarget,
153
+ path: resolvedPublishPath.relative,
154
+ pages: markdownFileCount,
155
+ frontmatterPolicy: publishFrontmatterPolicy,
156
+ cloned
157
+ }
158
+ };
159
+ }
160
+ catch (error) {
161
+ throw redactGitError(error, publishRemote);
162
+ }
163
+ finally {
164
+ await fs.rm(tempRoot, { recursive: true, force: true });
165
+ }
166
+ }
167
+ function sanitizeRemote(remote) {
168
+ if (!remote) {
169
+ return null;
170
+ }
171
+ return remote.replace(/([a-z][a-z\d+.-]*:\/\/)([^/?#@]+):([^/?#@]+)@/gi, '$1***:***@')
172
+ .replace(/([a-z][a-z\d+.-]*:\/\/)([^/?#@:]+)@/gi, '$1***@');
173
+ }
174
+ function redactGitError(error, remote) {
175
+ const redactedRemote = sanitizeRemote(remote);
176
+ const redact = (value) => {
177
+ if (typeof value !== 'string') {
178
+ return value;
179
+ }
180
+ const sanitized = sanitizeRemote(value) || value;
181
+ return remote && redactedRemote ? sanitized.split(remote).join(redactedRemote) : sanitized;
182
+ };
183
+ if (!(error instanceof Error)) {
184
+ return new Error(String(redact(error)));
185
+ }
186
+ const redactedError = new Error(String(redact(error.message)));
187
+ redactedError.name = error.name;
188
+ redactedError.stack = typeof error.stack === 'string' ? String(redact(error.stack)) : error.stack;
189
+ for (const key of ['code', 'signal', 'stdout', 'stderr', 'cmd']) {
190
+ const value = error[key];
191
+ if (value !== undefined) {
192
+ redactedError[key] = redact(value);
193
+ }
194
+ }
195
+ return redactedError;
196
+ }
197
+ function isCloneFallbackError(error) {
198
+ const details = [
199
+ error instanceof Error ? error.message : '',
200
+ typeof error?.stderr === 'string' ? error.stderr : ''
201
+ ].join('\n').toLowerCase();
202
+ return details.includes('remote branch') && details.includes('not found')
203
+ || details.includes('repository') && details.includes('not found')
204
+ || details.includes('repository') && details.includes('does not exist')
205
+ || details.includes('does not appear to be a git repository');
206
+ }
207
+ async function cleanCheckout(targetDir) {
208
+ await fs.mkdir(targetDir, { recursive: true });
209
+ const entries = await fs.readdir(targetDir, { withFileTypes: true });
210
+ await Promise.all(entries
211
+ .filter((entry) => entry.name !== '.git')
212
+ .map((entry) => fs.rm(path.join(targetDir, entry.name), { recursive: true, force: true })));
213
+ }
214
+ async function cleanPublishPath(checkoutDir, publishDir) {
215
+ if (checkoutDir === publishDir) {
216
+ await cleanCheckout(checkoutDir);
217
+ return;
218
+ }
219
+ await fs.rm(publishDir, { recursive: true, force: true });
220
+ await fs.mkdir(publishDir, { recursive: true });
221
+ }
222
+ async function cleanGeneratedMarkdown(publishDir) {
223
+ await fs.mkdir(publishDir, { recursive: true });
224
+ const entries = await fs.readdir(publishDir, { withFileTypes: true });
225
+ await Promise.all(entries.map(async (entry) => {
226
+ if (isReservedPublishEntry(entry.name)) {
227
+ return;
228
+ }
229
+ const entryPath = path.join(publishDir, entry.name);
230
+ if (entry.isDirectory()) {
231
+ await cleanGeneratedMarkdown(entryPath);
232
+ return;
233
+ }
234
+ if (entry.isFile() && entry.name.endsWith('.md') && !isPreservedPagesMarkdown(entry.name)) {
235
+ await fs.rm(entryPath, { force: true });
236
+ }
237
+ }));
238
+ }
239
+ function isReservedPublishEntry(name) {
240
+ return name === '.git' || name === '.github' || name === '_layouts';
241
+ }
242
+ function isPreservedPagesMarkdown(name) {
243
+ return name === 'index.md' || name === 'Navigation.md';
244
+ }
245
+ async function copyGeneratedWiki(sourceDir, targetDir, frontmatterPolicy, isPages = false) {
246
+ await fs.mkdir(targetDir, { recursive: true });
247
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
248
+ for (const entry of entries) {
249
+ if (entry.name === '.git') {
250
+ continue;
251
+ }
252
+ const source = path.join(sourceDir, entry.name);
253
+ const target = path.join(targetDir, entry.name);
254
+ if (entry.isSymbolicLink()) {
255
+ await copySymlink(source, target);
256
+ }
257
+ else if (entry.isDirectory()) {
258
+ await copyGeneratedWiki(source, target, frontmatterPolicy, isPages);
259
+ }
260
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
261
+ const content = await fs.readFile(source, 'utf8');
262
+ const transformed = applyFrontmatterPolicy(content, frontmatterPolicy);
263
+ const finalContent = isPages ? rewriteInternalWikiLinks(transformed) : transformed;
264
+ await fs.writeFile(target, finalContent, 'utf8');
265
+ }
266
+ else if (entry.isFile()) {
267
+ await fs.copyFile(source, target);
268
+ }
269
+ }
270
+ }
271
+ async function copySymlink(source, target) {
272
+ const linkTarget = await fs.readlink(source);
273
+ await fs.rm(target, { recursive: true, force: true });
274
+ await fs.symlink(linkTarget, target);
275
+ }
276
+ async function countMarkdownFiles(wikiDir) {
277
+ const entries = await fs.readdir(wikiDir, { withFileTypes: true });
278
+ const nestedCounts = await Promise.all(entries.map(async (entry) => {
279
+ const entryPath = path.join(wikiDir, entry.name);
280
+ if (entry.isDirectory()) {
281
+ return countMarkdownFiles(entryPath);
282
+ }
283
+ return entry.isFile() && entry.name.endsWith('.md') ? 1 : 0;
284
+ }));
285
+ return nestedCounts.reduce((total, count) => total + count, 0);
286
+ }
287
+ function resolvePublishRemote(target, remote) {
288
+ if (remote || process.env.LLMWIKI_PUBLISH_REMOTE) {
289
+ return remote || process.env.LLMWIKI_PUBLISH_REMOTE;
290
+ }
291
+ return target === 'github-wiki' ? process.env.GITHUB_WIKI_REMOTE : undefined;
292
+ }
293
+ function defaultBranchForTarget(target) {
294
+ return target === 'github-pages' ? 'gh-pages' : 'master';
295
+ }
296
+ export function defaultFrontmatterPolicyForTarget(target) {
297
+ return target === 'github-wiki' ? 'provenance' : 'preserve';
298
+ }
299
+ function assertSafeGitArgument(value, label) {
300
+ if (!value) {
301
+ return;
302
+ }
303
+ if (/^[\s-]/.test(value)) {
304
+ throw new Error(`Publish ${label} must not start with whitespace or "-".`);
305
+ }
306
+ if (/[\u0000\r\n]/.test(value)) {
307
+ throw new Error(`Publish ${label} contains unsupported control characters.`);
308
+ }
309
+ }
310
+ function resolvePublishPath(target, pagesPath) {
311
+ const rawPath = target === 'github-pages' ? (pagesPath || '.').trim() || '.' : '.';
312
+ if (/[\u0000\r\n]/.test(rawPath)) {
313
+ throw new Error('Publish path contains unsupported control characters.');
314
+ }
315
+ const pathForSegments = rawPath.replace(/\\/g, '/');
316
+ if (path.isAbsolute(pathForSegments) || path.posix.isAbsolute(pathForSegments)) {
317
+ throw new Error(`Publish path must be relative: ${rawPath}`);
318
+ }
319
+ const segments = pathForSegments.split('/').filter(Boolean);
320
+ if (segments.includes('..')) {
321
+ throw new Error(`Publish path must not contain ".." path segments: ${rawPath}`);
322
+ }
323
+ if (segments.some((segment) => segment.toLowerCase() === '.git')) {
324
+ throw new Error(`Publish path must not target reserved .git paths: ${rawPath}`);
325
+ }
326
+ const normalized = path.posix.normalize(pathForSegments).replace(/^\.\/+/, '') || '.';
327
+ return {
328
+ relative: normalized,
329
+ absoluteResolver: (checkoutDir) => path.resolve(checkoutDir, normalized)
330
+ };
331
+ }
332
+ function assertPublishPathContained(checkoutDir, publishDir) {
333
+ const relative = path.relative(checkoutDir, publishDir);
334
+ if (relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`))) {
335
+ return;
336
+ }
337
+ throw new Error(`Publish path must stay inside checkout: ${publishDir}`);
338
+ }
339
+ async function ensurePagesSiteSupport(siteRootDir, publishDir, pagesPath) {
340
+ await ensurePagesEntryAndNavigation(publishDir);
341
+ await ensurePagesNavInclude(siteRootDir, publishDir);
342
+ await ensurePagesMermaidSupport(siteRootDir, pagesPath);
343
+ }
344
+ async function ensurePagesEntryAndNavigation(publishDir) {
345
+ const homePath = path.join(publishDir, 'Home.md');
346
+ const indexPath = path.join(publishDir, 'index.md');
347
+ if (await fileExists(homePath) && !await fileExists(indexPath)) {
348
+ const homeContent = await fs.readFile(homePath, 'utf8');
349
+ await fs.writeFile(indexPath, homeContent, 'utf8');
350
+ }
351
+ const sidebarPath = path.join(publishDir, '_Sidebar.md');
352
+ const navigationPath = path.join(publishDir, 'Navigation.md');
353
+ if (await fileExists(sidebarPath) && !await fileExists(navigationPath)) {
354
+ const sidebarContent = await fs.readFile(sidebarPath, 'utf8');
355
+ await fs.writeFile(navigationPath, rewritePagesNavigationLinks(sidebarContent), 'utf8');
356
+ }
357
+ }
358
+ function rewritePagesNavigationLinks(content) {
359
+ return content.replace(/(?<!!)(\[[^\]]+\]\()([^\s)]+)(\))/g, (_match, prefix, target, suffix) => {
360
+ const rewritten = rewritePagesNavigationTarget(target);
361
+ return `${prefix}${rewritten}${suffix}`;
362
+ });
363
+ }
364
+ function rewritePagesNavigationTarget(target) {
365
+ if (!target || target.startsWith('#')) {
366
+ return target;
367
+ }
368
+ if (/^[a-z][a-z\d+.-]*:/i.test(target) || target.startsWith('//') || target.startsWith('/')) {
369
+ return target;
370
+ }
371
+ const [pathPart, hash = ''] = target.split('#', 2);
372
+ if (!pathPart || /[/?]/.test(pathPart)) {
373
+ return target;
374
+ }
375
+ if (path.extname(pathPart).toLowerCase() === '.md') {
376
+ return `${pathPart.slice(0, -3)}.html${hash ? `#${hash}` : ''}`;
377
+ }
378
+ if (path.extname(pathPart)) {
379
+ return target;
380
+ }
381
+ return `${pathPart}.html${hash ? `#${hash}` : ''}`;
382
+ }
383
+ async function ensurePagesMermaidSupport(siteRootDir, pagesPath) {
384
+ const configPath = path.join(siteRootDir, '_config.yml');
385
+ // Regenerate if new, marked as repo-wiki generated, or matching known legacy repo-wiki defaults.
386
+ // Preserve markerless files that do not match those legacy signatures as user-customized.
387
+ if (!await fileExists(configPath) || await isRepoWikiGeneratedFile(configPath, 'config')) {
388
+ await fs.writeFile(configPath, buildPagesConfig(pagesPath), 'utf8');
389
+ }
390
+ const layoutDir = path.join(siteRootDir, '_layouts');
391
+ const layoutPath = path.join(layoutDir, 'repo-wiki.html');
392
+ if (!await fileExists(layoutPath) || await isRepoWikiGeneratedFile(layoutPath, 'layout')) {
393
+ await fs.mkdir(layoutDir, { recursive: true });
394
+ await fs.writeFile(layoutPath, PAGES_LAYOUT, 'utf8');
395
+ }
396
+ }
397
+ function buildPagesConfig(pagesPath) {
398
+ const normalized = pagesPath === '.' ? '' : pagesPath;
399
+ const base = `${REPO_WIKI_GENERATED_YAML_COMMENT}\ndefaults:\n - scope:\n path: ""\n values:\n layout: "repo-wiki"\n`;
400
+ if (!normalized) {
401
+ return base;
402
+ }
403
+ // YAML-escape: backslashes first, then double-quotes to prevent YAML injection
404
+ const escaped = normalized.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
405
+ return `${base}wiki_pages_dir: "${escaped}"\n`;
406
+ }
407
+ async function ensurePagesNavInclude(siteRootDir, publishDir) {
408
+ const includesDir = path.join(siteRootDir, '_includes');
409
+ const navIncludePath = path.join(includesDir, 'wiki_nav.html');
410
+ // Preserve every markerless existing nav include as user-customized. Even markup
411
+ // resembling an older generated nav may have been edited by hand, so only files
412
+ // with the current repo-wiki marker are safe to refresh automatically.
413
+ if (await fileExists(navIncludePath) && !await isRepoWikiGeneratedFile(navIncludePath)) {
414
+ return;
415
+ }
416
+ let navBody = '';
417
+ const sidebarPath = path.join(publishDir, '_Sidebar.md');
418
+ const navMdPath = path.join(publishDir, 'Navigation.md');
419
+ if (await fileExists(sidebarPath)) {
420
+ navBody = parseSidebarToNavHtml(await fs.readFile(sidebarPath, 'utf8'));
421
+ }
422
+ else if (await fileExists(navMdPath)) {
423
+ navBody = parseSidebarToNavHtml(await fs.readFile(navMdPath, 'utf8'));
424
+ }
425
+ await fs.mkdir(includesDir, { recursive: true });
426
+ const navContent = navBody ? `${REPO_WIKI_GENERATED_HTML_COMMENT}\n${navBody}` : REPO_WIKI_GENERATED_HTML_COMMENT;
427
+ await fs.writeFile(navIncludePath, navContent, 'utf8');
428
+ }
429
+ /**
430
+ * Rewrite internal wiki-style links in markdown content so they use the
431
+ * published `.html` page URLs expected under GitHub Pages.
432
+ *
433
+ * Rules:
434
+ * - `[text](PageName)` → `[text](PageName.html)` (bare page-name link)
435
+ * - `[text](./Page.md)` → `[text](Page.html)` (normalize leading `./` and convert markdown source links)
436
+ * - External URLs, `mailto:`, anchor-only, and known asset extensions are left unchanged.
437
+ *
438
+ * Quantifiers are bounded to prevent polynomial backtracking on adversarial input.
439
+ */
440
+ export function rewriteInternalWikiLinks(content) {
441
+ return content.replace(/\[([^\]\n]{0,2048})\]\(([^)\n]{1,2048})\)/g, (match, text, href, offset) => {
442
+ if (offset > 0 && content[offset - 1] === '!') {
443
+ return match;
444
+ }
445
+ return `[${text}](${normalizeWikiHref(href)})`;
446
+ });
447
+ }
448
+ function isExternalOrAnchorHref(href) {
449
+ return /^(?:https?:|mailto:|ftp:|\/\/|#)/i.test(href.trim());
450
+ }
451
+ function normalizeWikiHref(href) {
452
+ const normalizedHref = href.trim();
453
+ // Reject unsafe URI schemes after trimming surrounding whitespace so leading
454
+ // spaces/tabs cannot bypass scheme detection in downstream markdown/HTML.
455
+ if (/^(?:javascript:|data:|vbscript:|blob:|about:)/i.test(normalizedHref)) {
456
+ return '#';
457
+ }
458
+ // Leave external links, mailto, anchor-only, and protocol-relative links unchanged
459
+ if (isExternalOrAnchorHref(normalizedHref)) {
460
+ return normalizedHref;
461
+ }
462
+ // Split off query string and fragment before applying extension logic
463
+ const hashIndex = normalizedHref.indexOf('#');
464
+ const queryIndex = normalizedHref.indexOf('?');
465
+ let separatorIndex;
466
+ if (queryIndex === -1) {
467
+ separatorIndex = hashIndex;
468
+ }
469
+ else if (hashIndex === -1) {
470
+ separatorIndex = queryIndex;
471
+ }
472
+ else {
473
+ separatorIndex = Math.min(queryIndex, hashIndex);
474
+ }
475
+ const pathPart = separatorIndex === -1 ? normalizedHref : normalizedHref.slice(0, separatorIndex);
476
+ const suffix = separatorIndex === -1 ? '' : normalizedHref.slice(separatorIndex);
477
+ // Leave known non-markdown asset extensions unchanged
478
+ if (/\.(?:png|jpe?g|gif|svg|webp|ico|pdf|txt|json|ya?ml|html?|css|js|ts)$/i.test(pathPart)) {
479
+ return normalizedHref;
480
+ }
481
+ // Normalize leading ./
482
+ let normalized = pathPart.replace(/^\.\//, '');
483
+ // Convert markdown source links to published HTML page URLs.
484
+ if (/\.md$/i.test(normalized)) {
485
+ normalized = normalized.replace(/\.md$/i, '.html');
486
+ }
487
+ else if (normalized && !/\.[a-z]{1,10}$/i.test(normalized) && !normalized.endsWith('/')) {
488
+ normalized += '.html';
489
+ }
490
+ return normalized + suffix;
491
+ }
492
+ /**
493
+ * Convert `_Sidebar.md` or `Navigation.md` content into a Liquid/HTML navigation
494
+ * fragment suitable for use as a Jekyll `_includes/wiki_nav.html` file.
495
+ *
496
+ * Internal hrefs are prefixed with `{{ _base }}` so they resolve correctly
497
+ * regardless of the page's depth in the site tree. External links and
498
+ * anchor-only links are left without the prefix.
499
+ */
500
+ function parseSidebarToNavHtml(content) {
501
+ const body = stripFrontmatter(content);
502
+ const lines = body.split('\n');
503
+ const parts = [];
504
+ let inList = false;
505
+ for (const line of lines) {
506
+ const headingMatch = /^#{1,4}\s+(.+)$/.exec(line);
507
+ if (headingMatch) {
508
+ if (inList) {
509
+ parts.push('</ul>');
510
+ inList = false;
511
+ }
512
+ parts.push(`<h4 class="nav-section">${escapeHtml(headingMatch[1].trim())}</h4>`);
513
+ continue;
514
+ }
515
+ const linkItemMatch = /^[ \t]*[-*+]\s+\[([^\]\n]{0,512})\]\(([^)\n]{0,512})\)/.exec(line);
516
+ if (linkItemMatch) {
517
+ if (!inList) {
518
+ parts.push('<ul>');
519
+ inList = true;
520
+ }
521
+ const text = escapeHtml(linkItemMatch[1]);
522
+ const rawHref = normalizeWikiHref(linkItemMatch[2]);
523
+ const isInternal = !isExternalOrAnchorHref(rawHref);
524
+ const escapedHref = escapeHtml(rawHref);
525
+ // Prefix internal links with {{ _base }} so they remain depth-relative.
526
+ // Jekyll evaluates the include in the layout's Liquid scope, so _base is available.
527
+ const href = isInternal ? `{{ _base }}${escapedHref}` : escapedHref;
528
+ parts.push(`<li><a href="${href}">${text}</a></li>`);
529
+ continue;
530
+ }
531
+ const plainItemMatch = /^[ \t]*[-*+]\s+(.+)$/.exec(line);
532
+ if (plainItemMatch) {
533
+ if (!inList) {
534
+ parts.push('<ul>');
535
+ inList = true;
536
+ }
537
+ parts.push(`<li>${escapeHtml(plainItemMatch[1].trim())}</li>`);
538
+ }
539
+ }
540
+ if (inList) {
541
+ parts.push('</ul>');
542
+ }
543
+ return parts.join('\n');
544
+ }
545
+ function escapeHtml(text) {
546
+ return text
547
+ .replace(/&/g, '&amp;')
548
+ .replace(/</g, '&lt;')
549
+ .replace(/>/g, '&gt;')
550
+ .replace(/"/g, '&quot;');
551
+ }
552
+ const PAGES_LAYOUT = `<!doctype html>
553
+ <!-- repo-wiki-generated: regenerated on each publish -->
554
+ <html lang="en">
555
+ <head>
556
+ <meta charset="utf-8">
557
+ <meta name="viewport" content="width=device-width, initial-scale=1">
558
+ <title>{% if page.title %}{{ page.title | escape }}{% else %}{{ page.name | replace: '.md', '' | escape }}{% endif %} &mdash; Wiki</title>
559
+ <style>
560
+ *, *::before, *::after { box-sizing: border-box; }
561
+ body { color: #24292f; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.5; margin: 0; }
562
+ .layout { display: flex; min-height: 100vh; }
563
+ .sidebar { background: #f6f8fa; border-right: 1px solid #d0d7de; flex-shrink: 0; overflow-y: auto; padding: 1rem; width: 220px; }
564
+ .sidebar-title { font-size: 0.875rem; font-weight: 600; margin-bottom: 0.5rem; }
565
+ .sidebar-title a { color: #24292f; text-decoration: none; }
566
+ .sidebar-title a:hover { color: #0969da; }
567
+ .site-nav ul { list-style: none; margin: 0; padding: 0; }
568
+ .site-nav li { margin: 0.15rem 0; }
569
+ .site-nav a { color: #0969da; font-size: 0.875rem; text-decoration: none; }
570
+ .site-nav a:hover { text-decoration: underline; }
571
+ .site-nav .nav-section { color: #57606a; font-size: 0.7rem; font-weight: 600; letter-spacing: 0.05em; margin: 0.75rem 0 0.25rem; text-transform: uppercase; }
572
+ .nav-divider { border: none; border-top: 1px solid #d0d7de; margin: 0.5rem 0; }
573
+ main { flex: 1; min-width: 0; max-width: 980px; padding: 2rem; }
574
+ .breadcrumb { color: #57606a; font-size: 0.875rem; margin-bottom: 1rem; }
575
+ .breadcrumb a { color: #0969da; text-decoration: none; }
576
+ .breadcrumb a:hover { text-decoration: underline; }
577
+ .page-metadata { background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; color: #57606a; font-size: 0.875rem; margin: 0 0 1.5rem; padding: 0.75rem 1rem; }
578
+ .page-metadata summary { color: #24292f; cursor: pointer; font-weight: 600; }
579
+ .page-metadata dl { display: grid; gap: 0.35rem 1rem; grid-template-columns: max-content minmax(0, 1fr); margin: 0.75rem 0 0; }
580
+ .page-metadata dt { color: #24292f; font-weight: 600; }
581
+ .page-metadata dd { margin: 0; min-width: 0; }
582
+ .page-metadata ul { margin: 0; padding-left: 1.25rem; }
583
+ .page-metadata code { color: #24292f; }
584
+ .back-link { border-top: 1px solid #d0d7de; color: #57606a; font-size: 0.875rem; margin-top: 2rem; padding-top: 1rem; }
585
+ .back-link a { color: #0969da; text-decoration: none; }
586
+ .back-link a:hover { text-decoration: underline; }
587
+ a { color: #0969da; text-decoration: none; }
588
+ a:hover { text-decoration: underline; }
589
+ pre { background: #f6f8fa; border-radius: 6px; overflow: auto; padding: 1rem; }
590
+ code { background: #f6f8fa; border-radius: 4px; padding: 0.1em 0.3em; }
591
+ pre code { background: transparent; padding: 0; }
592
+ table { border-collapse: collapse; display: block; overflow: auto; width: 100%; }
593
+ th, td { border: 1px solid #d0d7de; padding: 0.4rem 0.75rem; }
594
+ .mermaid { background: #fff; border: 1px solid #d0d7de; border-radius: 6px; margin: 1rem 0; padding: 1rem; }
595
+ @media (max-width: 768px) { .layout { flex-direction: column; } .sidebar { border-right: none; border-bottom: 1px solid #d0d7de; width: 100%; } }
596
+ </style>
597
+ </head>
598
+ <body>
599
+ {% assign _rp = page.path %}{% assign _wd = site.wiki_pages_dir | default: '.' %}{% if _wd != '.' %}{% assign _wd_prefix = _wd | append: '/' %}{% assign _rp = _rp | remove_first: _wd_prefix %}{% endif %}{% assign _depth = _rp | split: '/' | size | minus: 1 %}{% assign _base = '' %}{% for _i in (1.._depth) %}{% assign _base = _base | append: '../' %}{% endfor %}
600
+ <div class="layout">
601
+ <aside class="sidebar">
602
+ <div class="sidebar-title"><a href="{{ _base }}Home.html">Wiki</a></div>
603
+ <nav class="site-nav" aria-label="Site navigation">
604
+ <h4 class="nav-section">Quick links</h4>
605
+ <ul>
606
+ <li><a href="{{ _base }}Home.html">Home</a></li>
607
+ <li><a href="{{ _base }}Index.html">Index</a></li>
608
+ <li><a href="{{ _base }}Architecture.html">Architecture</a></li>
609
+ <li><a href="{{ _base }}Agent-Context-Pack.html">Agent Context Pack</a></li>
610
+ <li><a href="{{ _base }}Build-Test-and-Run.html">Build, Test &amp; Run</a></li>
611
+ <li><a href="{{ _base }}Documentation-Debt-Report.html">Documentation Debt</a></li>
612
+ </ul>
613
+ <hr class="nav-divider">
614
+ {% include wiki_nav.html %}
615
+ </nav>
616
+ </aside>
617
+ <main>
618
+ {% assign _kind = page.kind | default: '' %}{% if _kind == 'module' %}
619
+ <nav class="breadcrumb" aria-label="Breadcrumb">
620
+ <a href="{{ _base }}Home.html">Home</a> &rsaquo;
621
+ <a href="{{ _base }}Index.html">Index</a> &rsaquo;
622
+ <span>{{ page.title | default: page.name | replace: '.md', '' | escape }}</span>
623
+ </nav>{% elsif _kind != '' and _kind != 'home' %}
624
+ <nav class="breadcrumb" aria-label="Breadcrumb">
625
+ <a href="{{ _base }}Home.html">Home</a> &rsaquo;
626
+ <span>{{ page.title | default: page.name | replace: '.md', '' | escape }}</span>
627
+ </nav>{% endif %}
628
+ {% if page.page_state or page.kind or page.confidence or page.claim_status or page.source_repo or page.source_commit or page.compiled_at or page.source_paths %}
629
+ <details class="page-metadata">
630
+ <summary>Page metadata</summary>
631
+ <dl>
632
+ {% if page.kind %}<dt>Kind</dt><dd>{{ page.kind | escape }}</dd>{% endif %}
633
+ {% if page.page_state %}<dt>State</dt><dd>{{ page.page_state | escape }}</dd>{% endif %}
634
+ {% if page.confidence %}<dt>Confidence</dt><dd>{{ page.confidence | escape }}</dd>{% endif %}
635
+ {% if page.claim_status %}<dt>Claim status</dt><dd>{{ page.claim_status | escape }}</dd>{% endif %}
636
+ {% if page.source_repo %}<dt>Source repo</dt><dd>{{ page.source_repo | escape }}</dd>{% endif %}
637
+ {% if page.source_commit %}<dt>Source commit</dt><dd><code>{{ page.source_commit | escape }}</code></dd>{% endif %}
638
+ {% if page.compiled_at %}<dt>Compiled at</dt><dd>{{ page.compiled_at | escape }}</dd>{% endif %}
639
+ {% if page.source_paths %}<dt>Source paths</dt><dd><ul>{% for source_path in page.source_paths %}<li><code>{{ source_path | escape }}</code></li>{% endfor %}</ul></dd>{% endif %}
640
+ </dl>
641
+ </details>
642
+ {% endif %}
643
+ {{ content }}
644
+ {% if _kind != 'home' %}<div class="back-link"><a href="{{ _base }}Index.html">&larr; Back to Index</a></div>{% endif %}
645
+ </main>
646
+ </div>
647
+ <script type="module">
648
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
649
+ mermaid.initialize({ startOnLoad: false, securityLevel: 'strict' });
650
+ const blocks = document.querySelectorAll('pre > code.language-mermaid');
651
+ blocks.forEach((block) => {
652
+ const container = document.createElement('div');
653
+ container.className = 'mermaid';
654
+ container.textContent = block.textContent || '';
655
+ block.parentElement?.replaceWith(container);
656
+ });
657
+ await mermaid.run({ nodes: document.querySelectorAll('.mermaid') });
658
+ </script>
659
+ </body>
660
+ </html>
661
+ `;
662
+ //# sourceMappingURL=publisher.js.map