kennel 0.1.8 → 0.1.9

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 (168) hide show
  1. package/README.md +36 -87
  2. package/bin/kennel-native.mjs +5 -0
  3. package/dist/cli/dispatch.js +25 -16
  4. package/dist/cli/usage.js +9 -3
  5. package/dist/deploy/cloudflare.js +10 -1
  6. package/dist/deploy/smoke-tests.d.ts +1 -0
  7. package/dist/deploy/smoke-tests.js +21 -0
  8. package/dist/deploy/vercel.d.ts +3 -0
  9. package/dist/deploy/vercel.js +67 -0
  10. package/dist/mcp/config.js +14 -0
  11. package/dist/mcp/function-source.js +3 -1
  12. package/dist/mcp/generate.js +3 -3
  13. package/dist/mcp/install-page.d.ts +12 -1
  14. package/dist/mcp/install-page.js +3 -358
  15. package/dist/notifications/email-worker.js +1 -1
  16. package/dist/publish/cloudflare-init.js +5 -2
  17. package/dist/publish/config.d.ts +1 -1
  18. package/dist/publish/config.js +3 -15
  19. package/dist/publish/vercel-init.d.ts +9 -0
  20. package/dist/publish/vercel-init.js +52 -0
  21. package/dist/site/dev.js +2 -4
  22. package/dist/site/extension.d.ts +8 -0
  23. package/dist/site/extension.js +114 -0
  24. package/dist/site/renderers.d.ts +61 -0
  25. package/dist/site/renderers.js +219 -0
  26. package/dist/site/viewer.d.ts +9 -0
  27. package/dist/site/viewer.js +192 -0
  28. package/dist/site/vitepress.d.ts +1 -21
  29. package/dist/site/vitepress.js +1 -66
  30. package/dist/sources/commands.d.ts +1 -0
  31. package/dist/sources/commands.js +88 -0
  32. package/package.json +8 -8
  33. package/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  34. package/web/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  35. package/web/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  36. package/web/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  37. package/web/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  38. package/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  39. package/web/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  40. package/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  41. package/web/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  42. package/web/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  43. package/web/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  44. package/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  45. package/web/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  46. package/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  47. package/web/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  48. package/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  49. package/web/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  50. package/web/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  51. package/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  52. package/web/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  53. package/web/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  54. package/web/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  55. package/web/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  56. package/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  57. package/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  58. package/web/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  59. package/web/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  60. package/web/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  61. package/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  62. package/web/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  63. package/web/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  64. package/web/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  65. package/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  66. package/web/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  67. package/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  68. package/web/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  69. package/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  70. package/web/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  71. package/web/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  72. package/web/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  73. package/web/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  74. package/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  75. package/web/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  76. package/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  77. package/web/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  78. package/web/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  79. package/web/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  80. package/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  81. package/web/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  82. package/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  83. package/web/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  84. package/web/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  85. package/web/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  86. package/web/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  87. package/web/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  88. package/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  89. package/web/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  90. package/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  91. package/web/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  92. package/web/assets/abnfDiagram-VRR7QNED-Dizo1F0V.js +1 -0
  93. package/web/assets/arc-CraBhW5A.js +1 -0
  94. package/web/assets/architectureDiagram-ZJ3FMSHR-DrcQCjV4.js +36 -0
  95. package/web/assets/blockDiagram-677ZJIJ3-iGvS8HCJ.js +132 -0
  96. package/web/assets/c4Diagram-LMCZKHZV-B9ueRzOf.js +10 -0
  97. package/web/assets/channel-DjVeob-k.js +1 -0
  98. package/web/assets/chunk-2Q5K7J3B-Dg02y5Go.js +1 -0
  99. package/web/assets/chunk-32BRIVSS-DHUNfpw6.js +1 -0
  100. package/web/assets/chunk-5VM5RSS4-B6XJu9sr.js +15 -0
  101. package/web/assets/chunk-EX3LRPZG-DywY4I3l.js +231 -0
  102. package/web/assets/chunk-JWPE2WC7-C9-XkXDg.js +1 -0
  103. package/web/assets/chunk-MOJQB5TN-CXQXOtY9.js +88 -0
  104. package/web/assets/chunk-RYQCIY6F-DJwv9114.js +1 -0
  105. package/web/assets/chunk-V7JOEXUC-_b44nhwN.js +206 -0
  106. package/web/assets/chunk-VR4S4FIN-B_bGSDri.js +1 -0
  107. package/web/assets/chunk-XXDRQBXY-Bf-JPvQi.js +1 -0
  108. package/web/assets/classDiagram-OUVF2IWQ-4Od2_pq4.js +1 -0
  109. package/web/assets/classDiagram-v2-EOCWNBFH-4Od2_pq4.js +1 -0
  110. package/web/assets/cose-bilkent-JH36ORCC-Bcssb1jB.js +1 -0
  111. package/web/assets/cynefin-VYW2F7L2-t9uGqnJ4.js +166 -0
  112. package/web/assets/cynefinDiagram-TSTJHNR4-D0RvbNfG.js +62 -0
  113. package/web/assets/cytoscape.esm-D3_iZ_3b.js +321 -0
  114. package/web/assets/dagre-VKFMJZFB-DmmlPxN_.js +4 -0
  115. package/web/assets/defaultLocale-C4B-KCzX.js +1 -0
  116. package/web/assets/diagram-FQU43EPY-DtDak7gR.js +3 -0
  117. package/web/assets/diagram-G47NLZAW-Cn07lZ4L.js +24 -0
  118. package/web/assets/diagram-NH7WQ7WH-BTD9-weZ.js +24 -0
  119. package/web/assets/diagram-OA4YK3LP-fjOdFedY.js +30 -0
  120. package/web/assets/diagram-WEI45ONY-DSBYO7Nc.js +41 -0
  121. package/web/assets/ebnfDiagram-CCIWWBDH-B8km6QWT.js +1 -0
  122. package/web/assets/erDiagram-Q63AITRT-CHs2HT-U.js +85 -0
  123. package/web/assets/flowDiagram-23GEKE2U-CxjQgYus.js +156 -0
  124. package/web/assets/ganttDiagram-NO4QXBWP-CHkGy5_G.js +292 -0
  125. package/web/assets/gitGraphDiagram-IHSO6WYX-BdtSK3Q5.js +106 -0
  126. package/web/assets/graph-BvS0Xx32.js +1 -0
  127. package/web/assets/index-BiSkqJri.css +1 -0
  128. package/web/assets/index-sG3Cxc28.js +697 -0
  129. package/web/assets/infoDiagram-FWYZ7A6U-B0feeRLu.js +2 -0
  130. package/web/assets/init-Gi6I4Gst.js +1 -0
  131. package/web/assets/ishikawaDiagram-FXEZZL3T-DGCSsJ4r.js +70 -0
  132. package/web/assets/journeyDiagram-5HDEW3XC-D9xtvo9Z.js +139 -0
  133. package/web/assets/kanban-definition-HUTT4EX6-BqVdsgmq.js +89 -0
  134. package/web/assets/layout-D4ui-CWC.js +1 -0
  135. package/web/assets/linear-BopdAHdJ.js +1 -0
  136. package/web/assets/map-7eTsl7HP.js +1 -0
  137. package/web/assets/mermaid.core-DWAi8MqL.js +308 -0
  138. package/web/assets/mindmap-definition-LN4V7U3C-CsV_gb2j.js +96 -0
  139. package/web/assets/ordinal-Cboi1Yqb.js +1 -0
  140. package/web/assets/pegDiagram-2B236MQR-BmDNuFL1.js +1 -0
  141. package/web/assets/pieDiagram-ENE6RG2P-CBPZlXZP.js +39 -0
  142. package/web/assets/quadrantDiagram-ABIIQ3AL-CbUemD3z.js +7 -0
  143. package/web/assets/railroadDiagram-RFXS5EU6-Cyp4W7Hm.js +1 -0
  144. package/web/assets/requirementDiagram-TGXJPOKE-Dz35_qtT.js +84 -0
  145. package/web/assets/sankeyDiagram-HTMAVEWB-biUDOCWc.js +40 -0
  146. package/web/assets/sequenceDiagram-DBY2YBRQ-Bk4X1-E4.js +162 -0
  147. package/web/assets/sizeCapture-X5ZJPWSS-CDPSVeoQ.js +1 -0
  148. package/web/assets/stateDiagram-2N3HPSRC-Cx_G_F7H.js +1 -0
  149. package/web/assets/stateDiagram-v2-6OUMAXLB-DG9cZ_-X.js +1 -0
  150. package/web/assets/swimlanes-5IMT3BWC-KfhGp3gJ.js +2 -0
  151. package/web/assets/swimlanesDiagram-G3AALYLV-Bj6WXCUy.js +8 -0
  152. package/web/assets/timeline-definition-FHXFAJF6-Dp6wlW_7.js +120 -0
  153. package/web/assets/vennDiagram-L72KCM5P-CjdOdrf1.js +34 -0
  154. package/web/assets/wardleyDiagram-EHGQE667-DYl5GkQs.js +78 -0
  155. package/web/assets/xychartDiagram-FW5EYKEG-rPJ1RPYD.js +7 -0
  156. package/web/index.html +14 -0
  157. package/dist/mcp/data.d.ts +0 -14
  158. package/dist/mcp/data.js +0 -120
  159. package/dist/schema/doctor.d.ts +0 -1
  160. package/dist/schema/doctor.js +0 -69
  161. package/dist/schema/documents.d.ts +0 -18
  162. package/dist/schema/documents.js +0 -127
  163. package/dist/schema/pointers.d.ts +0 -5
  164. package/dist/schema/pointers.js +0 -48
  165. package/dist/schema/types.d.ts +0 -36
  166. package/dist/schema/types.js +0 -1
  167. package/dist/schema/validation.d.ts +0 -8
  168. package/dist/schema/validation.js +0 -212
@@ -1,360 +1,5 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { dirname, relative, resolve } from 'node:path';
3
- import { isObject } from '../shared/object.js';
4
- const defaultSources = [
5
- { text: 'Claude Code deep links', url: 'https://code.claude.com/docs/en/deep-links' },
6
- { text: 'Claude Code MCP', url: 'https://code.claude.com/docs/en/mcp' },
7
- { text: 'Codex app deep links', url: 'https://developers.openai.com/codex/app/commands#deep-links' },
8
- { text: 'Codex MCP', url: 'https://developers.openai.com/codex/mcp' },
9
- { text: 'Cursor deep links', url: 'https://cursor.com/docs/reference/deeplinks' },
10
- { text: 'Cursor MCP', url: 'https://cursor.com/docs/mcp' },
11
- {
12
- text: 'Copilot Studio MCP onboarding',
13
- url: 'https://learn.microsoft.com/en-us/microsoft-copilot-studio/mcp-add-existing-server-to-agent',
14
- },
15
- {
16
- text: 'Microsoft 365 agent connectors for MCP',
17
- url: 'https://learn.microsoft.com/en-us/microsoftteams/platform/m365-apps/agent-connectors',
18
- },
19
- ];
20
- function codeBlock(language, source) {
21
- return [`\`\`\`${language}`, source, '```'].join('\n');
22
- }
23
- function frontmatter(title, description) {
24
- return ['---', `title: ${JSON.stringify(title)}`, `description: ${JSON.stringify(description)}`, '---'].join('\n');
25
- }
26
- function escapeHtmlAttribute(value) {
27
- return String(value).replace(/&/g, '&').replace(/"/g, '"');
28
- }
29
- function normalizeBasePath(base) {
30
- if (typeof base !== 'string' || !base.trim() || base === '/')
31
- return '/';
32
- return `/${base.replace(/^\/+|\/+$/g, '')}/`;
33
- }
34
- function publicSiteUrl(config) {
35
- const publication = config.publication ?? {};
36
- const domains = publication.domains ?? {};
37
- const url = config.url
38
- ?? config.siteUrl
39
- ?? publication.url
40
- ?? domains.primary
41
- ?? publication.hostname
42
- ?? config.hostname;
43
- if (typeof url !== 'string' || !url.trim())
44
- return undefined;
45
- const trimmed = url.trim().replace(/\/+$/, '');
46
- return /^https?:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
47
- }
48
- function publicMcpSiteUrl(config) {
49
- const publicUrl = publicMcpEndpoint(config);
50
- if (!publicUrl)
51
- return undefined;
52
- try {
53
- return new URL(publicUrl).origin;
54
- }
55
- catch {
56
- return undefined;
57
- }
58
- }
59
- function list(items = []) {
60
- return items.map((item) => ` - \`${item}\``).join('\n');
61
- }
62
- function enabledInstallConfig(config) {
63
- if (!isObject(config.install) || config.install.enabled === false)
64
- return null;
65
- if (config.install.enabled !== true && !publicMcpEndpoint(config))
66
- return null;
67
- return config.install;
68
- }
69
- function mcpEndpoints(config) {
70
- const endpoints = isObject(config.mcp?.endpoints) ? config.mcp.endpoints : {};
71
- const installMcp = isObject(config.install?.mcp) ? config.install.mcp : {};
72
- return {
73
- portal: stringValue(endpoints.portal),
74
- protected: stringValue(installMcp.protectedUrl) ?? stringValue(endpoints.protected),
75
- public: stringValue(installMcp.publicUrl) ?? stringValue(endpoints.public),
76
- };
77
- }
78
- function publicMcpEndpoint(config) {
79
- return mcpEndpoints(config).public;
80
- }
81
- function stringValue(value) {
82
- return typeof value === 'string' && value.trim() ? value.trim() : undefined;
83
- }
84
- function installMarkdownUrl(config) {
85
- const siteUrl = publicMcpSiteUrl(config) ?? publicSiteUrl(config);
86
- if (!siteUrl)
87
- return undefined;
88
- return `${siteUrl}${normalizeBasePath(config.base)}${installPagePath(config).replace(/^\/+/, '')}`;
89
- }
90
- function appGuideUrl(baseUrl, client) {
91
- if (!baseUrl)
92
- return undefined;
93
- const fragment = client === 'Claude Code' ? 'claude-code' : client.toLowerCase();
94
- return `${baseUrl.replace(/#.*$/, '')}#${fragment}`;
95
- }
96
- function quickStartPrompt(quickStart = {}, mcp, client) {
97
- const promptKey = client === 'Claude Code' ? 'claudePrompt' : client === 'Cursor' ? 'cursorPrompt' : 'codexPrompt';
98
- if (typeof quickStart[promptKey] === 'string')
99
- return quickStart[promptKey];
100
- if (typeof quickStart.prompt === 'string')
101
- return quickStart.prompt;
102
- const setupStep = client === 'Claude Code'
103
- ? `Run: claude mcp add --transport http ${mcp.serverName} ${mcp.publicUrl}`
104
- : client === 'Cursor'
105
- ? `Add hosted MCP server ${mcp.serverName}: ${mcp.publicUrl}`
106
- : `Run: codex mcp add ${mcp.serverName} --url ${mcp.publicUrl}`;
107
- const guideUrl = appGuideUrl(mcp.installMarkdownUrl, client);
108
- return [
109
- `Set up ${mcp.displayName} in ${client}.`,
110
- guideUrl ? `Guide: ${guideUrl}` : undefined,
111
- setupStep,
112
- `Verify tools; search ${mcp.displayName} for "${client} MCP setup". If blocked, give the next command.`,
113
- ].filter(Boolean).join('\n');
114
- }
115
- function quickStartCard(input) {
116
- return [
117
- `<a href="${escapeHtmlAttribute(input.href)}" style="display:flex;flex:1 1 0;min-width:180px;gap:12px;align-items:center;justify-content:center;min-height:64px;border:1px solid var(--vp-c-brand-1);border-radius:8px;padding:12px 16px;text-decoration:none;color:var(--vp-c-text-1);background:var(--vp-c-bg-soft);cursor:pointer;font-weight:700;transition:border-color .15s ease,background-color .15s ease,transform .15s ease;">`,
118
- ` <span aria-hidden="true" style="display:grid;place-items:center;flex:0 0 auto;width:34px;height:34px;border-radius:8px;background:var(--vp-c-bg);color:${input.color};">${input.icon}</span>`,
119
- ` <span>${input.label}</span>`,
120
- '</a>',
121
- ].join('\n');
122
- }
123
- function quickStartSection(quickStart = {}, mcp) {
124
- if (quickStart.enabled === false)
125
- return [];
126
- const codexPrompt = quickStartPrompt(quickStart, mcp, 'Codex');
127
- const claudePrompt = quickStartPrompt(quickStart, mcp, 'Claude Code');
128
- const cursorPrompt = quickStartPrompt(quickStart, mcp, 'Cursor');
129
- const codexUrl = `codex://new?prompt=${encodeURIComponent(codexPrompt)}`;
130
- const claudeUrl = `claude://claude.ai/new?q=${encodeURIComponent(claudePrompt)}`;
131
- const cursorUrl = `cursor://anysphere.cursor-deeplink/prompt?text=${encodeURIComponent(cursorPrompt)}`;
132
- const codexIcon = '<svg width="28" height="28" viewBox="0 0 24 24" role="img" aria-label="OpenAI"><path fill="currentColor" d="M22.282 9.821a6 6 0 0 0-.516-4.91a6.05 6.05 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a6 6 0 0 0-3.998 2.9a6.05 6.05 0 0 0 .743 7.097a5.98 5.98 0 0 0 .51 4.911a6.05 6.05 0 0 0 6.515 2.9A6 6 0 0 0 13.26 24a6.06 6.06 0 0 0 5.772-4.206a6 6 0 0 0 3.997-2.9a6.06 6.06 0 0 0-.747-7.073M13.26 22.43a4.48 4.48 0 0 1-2.876-1.04l.141-.081l4.779-2.758a.8.8 0 0 0 .392-.681v-6.737l2.02 1.168a.07.07 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494M3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085l4.783 2.759a.77.77 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646M2.34 7.896a4.5 4.5 0 0 1 2.366-1.973V11.6a.77.77 0 0 0 .388.677l5.815 3.354l-2.02 1.168a.08.08 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.08.08 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667m2.01-3.023l-.141-.085l-4.774-2.782a.78.78 0 0 0-.785 0L9.409 9.23V6.897a.07.07 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.8.8 0 0 0-.393.681zm1.097-2.365l2.602-1.5l2.607 1.5v2.999l-2.597 1.5l-2.607-1.5Z"/></svg>';
133
- const claudeIcon = '<svg width="28" height="28" viewBox="0 0 24 24" role="img" aria-label="Claude"><path fill="currentColor" d="m4.714 15.956l4.718-2.648l.079-.23l-.08-.128h-.23l-.79-.048l-2.695-.073l-2.337-.097l-2.265-.122l-.57-.121l-.535-.704l.055-.353l.48-.321l.685.06l1.518.104l2.277.157l1.651.098l2.447.255h.389l.054-.158l-.133-.097l-.103-.098l-2.356-1.596l-2.55-1.688l-1.336-.972l-.722-.491L2 6.223l-.158-1.008l.656-.722l.88.06l.224.061l.893.686l1.906 1.476l2.49 1.833l.364.304l.146-.104l.018-.072l-.164-.274l-1.354-2.446l-1.445-2.49l-.644-1.032l-.17-.619a3 3 0 0 1-.103-.729L6.287.133L6.7 0l.995.134l.42.364l.619 1.415L9.735 4.14l1.555 3.03l.455.898l.243.832l.09.255h.159V9.01l.127-1.706l.237-2.095l.23-2.695l.08-.76l.376-.91l.747-.492l.583.28l.48.685l-.067.444l-.286 1.851l-.558 2.903l-.365 1.942h.213l.243-.242l.983-1.306l1.652-2.064l.728-.82l.85-.904l.547-.431h1.032l.759 1.129l-.34 1.166l-1.063 1.347l-.88 1.142l-1.263 1.7l-.79 1.36l.074.11l.188-.02l2.853-.606l1.542-.28l1.84-.315l.832.388l.09.395l-.327.807l-1.967.486l-2.307.462l-3.436.813l-.043.03l.049.061l1.548.146l.662.036h1.62l3.018.225l.79.522l.473.638l-.08.485l-1.213.62l-1.64-.389l-3.825-.91l-1.31-.329h-.183v.11l1.093 1.068l2.003 1.81l2.508 2.33l.127.578l-.321.455l-.34-.049l-2.204-1.657l-.85-.747l-1.925-1.62h-.127v.17l.443.649l2.343 3.521l.122 1.08l-.17.353l-.607.213l-.668-.122l-1.372-1.924l-1.415-2.168l-1.141-1.943l-.14.08l-.674 7.254l-.316.37l-.728.28l-.607-.461l-.322-.747l.322-1.476l.388-1.924l.316-1.53l.285-1.9l.17-.632l-.012-.042l-.14.018l-1.432 1.967l-2.18 2.945l-1.724 1.845l-.413.164l-.716-.37l.066-.662l.401-.589l2.386-3.036l1.439-1.882l.929-1.086l-.006-.158h-.055L4.138 18.56l-1.13.146l-.485-.456l.06-.746l.231-.243l1.907-1.312Z"/></svg>';
134
- const cards = [
135
- quickStartCard({
136
- href: codexUrl,
137
- label: quickStart.codexLabel ?? 'OpenAI Codex',
138
- icon: codexIcon,
139
- color: '#111111',
140
- }),
141
- quickStartCard({
142
- href: claudeUrl,
143
- label: quickStart.claudeLabel ?? 'Claude Code',
144
- icon: claudeIcon,
145
- color: '#D97757',
146
- }),
147
- quickStartCard({
148
- href: cursorUrl,
149
- label: quickStart.cursorLabel ?? 'Cursor',
150
- icon: '<svg width="28" height="28" viewBox="0 0 24 24" role="img" aria-label="Cursor"><path fill="currentColor" d="M12 2 3 7.25v9.5L12 22l9-5.25v-9.5zm0 2.32 6.98 4.07-4.17 2.44L12 9.19 9.19 10.83 5.02 8.39zm-7 5.8 4 2.34v4.65l-4-2.34zm6 9.28v-4.66l1 .59 1-.59v4.66l-1 .58zm3-2.29v-4.65l4-2.34v4.65zm-.99-5.25L12 12.45l-1.01-.59L12 11.27z"/></svg>',
151
- color: '#111111',
152
- }),
153
- ];
154
- const layoutStyle = cards.length >= 4
155
- ? 'display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin:20px 0;'
156
- : 'display:flex;flex-wrap:wrap;gap:12px;margin:20px 0;';
157
- return [
158
- '## AI Quick Start',
159
- '',
160
- `<div class="ai-quick-start" style="${layoutStyle}">`,
161
- ...cards,
162
- '</div>',
163
- ];
164
- }
165
- function installPagePath(config) {
166
- const install = enabledInstallConfig(config);
167
- const configured = String(install?.path ?? 'install.md').replace(/^\/+/, '');
168
- if (!configured || configured.split('/').includes('..'))
169
- throw new Error(`Invalid install.path: ${install?.path}`);
170
- return configured.endsWith('.md') ? configured : `${configured}.md`;
171
- }
172
- function repositoryPrompt(config, displayName, remoteUrl) {
173
- return config.setupPrompt ?? [
174
- `Set up the ${displayName} MCP server from this repository.`,
175
- 'Read mcp/README.md, run npm install and npm run build in mcp/, then configure this local stdio MCP server for this client.',
176
- `Use the Git remote ${remoteUrl}.`,
177
- ].join(' ');
178
- }
179
- function cloudflarePortalSection(portal = {}, mcp) {
180
- if (portal.enabled === false || !portal.url)
181
- return [];
182
- const serverId = portal.serverId ?? mcp.serverName;
183
- const upstreamUrl = portal.upstreamUrl ?? mcp.publicUrl;
184
- const portalId = portal.portalId ?? `${serverId}-portal`;
185
- const hostname = portal.hostname ?? new URL(String(portal.url)).hostname;
186
- const gatewayCname = portal.gatewayCname ?? 'gateway.agents.cloudflare.com';
187
- return [
188
- '## Cloudflare Managed Portal',
189
- '',
190
- "Cloudflare MCP server portals are managed under **Zero Trust** > **Access controls** > **AI controls**. They require a dedicated hostname that points at Cloudflare's MCP portal gateway, then expose MCP at `/mcp`.",
191
- '',
192
- 'Target portal:',
193
- '',
194
- codeBlock('text', String(portal.url)),
195
- '',
196
- 'Upstream MCP server:',
197
- '',
198
- codeBlock('text', upstreamUrl),
199
- '',
200
- 'Cloudflare setup:',
201
- '',
202
- `1. Add an MCP server with server ID \`${serverId}\`, authentication \`${portal.authentication ?? 'Unauthenticated'}\`, and HTTP URL \`${upstreamUrl}\`.`,
203
- `2. Create an MCP server portal with portal ID \`${portalId}\`, hostname \`${hostname}\`, and include the \`${serverId}\` server.`,
204
- `3. Create a proxied CNAME for \`${hostname}\` pointing to \`${gatewayCname}\` if the portal was created through the API or Terraform.`,
205
- `4. Add Access policies for the portal and for the \`${serverId}\` MCP server entry so allowed users can see the server in the portal.`,
206
- '',
207
- 'Equivalent Cloudflare API endpoints:',
208
- '',
209
- codeBlock('text', 'POST /accounts/{account_id}/access/ai-controls/mcp/servers\nPOST /accounts/{account_id}/access/ai-controls/mcp/portals'),
210
- '',
211
- portal.accessNote ?? 'Use a Cloudflare token with Zero Trust Access/AI Controls write access before provisioning the managed portal.',
212
- '',
213
- 'For Claude or other MCP clients that need browser-based portal auth, Cloudflare recommends `mcp-remote`:',
214
- '',
215
- codeBlock('json', JSON.stringify({ mcpServers: { [serverId]: { command: 'npx', args: ['-y', 'mcp-remote@latest', portal.url] } } }, null, 2)),
216
- ];
217
- }
218
- function localRepositorySection(repository = {}, mcp) {
219
- if (repository.enabled === false || !repository.remoteUrl || !repository.localPath)
220
- return [];
221
- const prompt = repositoryPrompt(repository, mcp.displayName, repository.remoteUrl);
222
- const codexUrl = `codex://new?originUrl=${encodeURIComponent(repository.remoteUrl)}&prompt=${encodeURIComponent(prompt)}`;
223
- const claudeUrl = `claude-cli://open?cwd=${encodeURIComponent(repository.localPath)}&q=${encodeURIComponent(prompt)}`;
224
- const configuredServerDirectory = repository.localServerDirectory ?? 'mcp';
225
- const serverDirectory = String(configuredServerDirectory).startsWith('/')
226
- ? String(configuredServerDirectory)
227
- : `${String(repository.localPath).replace(/\/+$/, '')}/${String(configuredServerDirectory).replace(/^\/+/, '')}`;
228
- const localServerCommand = repository.localServerCommand ?? `node ${serverDirectory}/dist/server.js`;
229
- return [
230
- '## Local Repository Setup',
231
- '',
232
- 'Use these links only when you want to open the Git-backed repository in an agent and configure the local MCP server from `mcp/`.',
233
- '',
234
- `- [Open in Codex](${codexUrl}): uses Codex \`originUrl\` matching to find a local workspace with the \`${repository.remoteUrl}\` remote.`,
235
- `- [Open in Claude Code](${claudeUrl}): uses Claude Code \`cwd\` matching for this local clone. Claude Code's \`repo=\` deeplink is GitHub \`owner/name\` based, so use \`cwd=\` for non-GitHub repositories.`,
236
- '',
237
- 'If the clone is not present locally:',
238
- '',
239
- codeBlock('bash', `git clone ${repository.remoteUrl}.git ${repository.localPath}`),
240
- '',
241
- 'Configure the local stdio MCP server:',
242
- '',
243
- codeBlock('bash', [
244
- `claude mcp add --transport stdio ${mcp.serverName} -- ${localServerCommand}`,
245
- `codex mcp add ${mcp.serverName} -- ${localServerCommand}`,
246
- ].join('\n')),
247
- ];
248
- }
249
- function sourceSection(sources = defaultSources) {
250
- return sources.length ? [`Sources checked: ${sources.map((source) => `[${source.text}](${source.url})`).join(', ')}.`] : [];
251
- }
252
- function renderInstallPage(config) {
253
- const install = enabledInstallConfig(config);
254
- if (!install)
255
- return undefined;
256
- const endpoints = mcpEndpoints(config);
257
- const coreMcp = isObject(config.mcp) ? config.mcp : {};
258
- const installMcp = isObject(install.mcp) ? install.mcp : {};
259
- const mcp = {
260
- description: installMcp.description ?? config.description ?? '',
261
- displayName: installMcp.displayName ?? install.title ?? config.title ?? 'Index',
262
- protectedUrl: endpoints.protected ?? '',
263
- publicUrl: endpoints.public ?? '',
264
- serverName: installMcp.serverName ?? coreMcp.serverName ?? config.name ?? 'index',
265
- tools: Array.isArray(installMcp.tools) ? installMcp.tools.map(String) : [],
266
- installMarkdownUrl: installMarkdownUrl(config),
267
- };
268
- if (!mcp.publicUrl)
269
- throw new Error('mcp.endpoints.public or install.mcp.publicUrl is required when install is enabled');
270
- const title = install.title ?? 'Install';
271
- const description = install.description ?? `Connect ${mcp.displayName} MCP to Codex, Claude, Microsoft Teams, and Copilot.`;
272
- const lines = [
273
- frontmatter(title, description),
274
- '',
275
- `# ${title}`,
276
- '',
277
- ...quickStartSection(install.quickStart, mcp),
278
- '',
279
- 'Use the public Cloudflare MCP endpoint when you want an agent to use this knowledge base without Git credentials or a local clone.',
280
- '',
281
- codeBlock('text', mcp.publicUrl),
282
- '',
283
- mcp.protectedUrl ? `The custom domain is protected by Cloudflare Access, so use \`${mcp.publicUrl}\` for no-creds agent setup.` : undefined,
284
- '',
285
- ...cloudflarePortalSection({
286
- url: endpoints.portal,
287
- upstreamUrl: endpoints.public,
288
- ...install.cloudflarePortal,
289
- }, mcp),
290
- '',
291
- '## Codex',
292
- '',
293
- 'Add the hosted MCP server:',
294
- '',
295
- codeBlock('bash', `codex mcp add ${mcp.serverName} --url ${mcp.publicUrl}`),
296
- '',
297
- `Restart Codex Desktop or open a new thread, then run \`/mcp\` to confirm \`${mcp.serverName}\` is connected.`,
298
- '',
299
- '## Claude Code',
300
- '',
301
- 'Add the hosted MCP server:',
302
- '',
303
- codeBlock('bash', `claude mcp add --transport http ${mcp.serverName} ${mcp.publicUrl}`),
304
- '',
305
- 'Restart Claude Code or open a new session. The tools should appear as:',
306
- '',
307
- codeBlock('text', mcp.tools.map((tool) => `mcp__${mcp.serverName}__${tool}`).join('\n')),
308
- '',
309
- '## Microsoft Teams and Copilot',
310
- '',
311
- 'Use the same public MCP endpoint for Microsoft 365 Copilot and Teams agent experiments:',
312
- '',
313
- codeBlock('text', mcp.publicUrl),
314
- '',
315
- 'Fastest Copilot Studio path:',
316
- '',
317
- '1. Open the target agent in Copilot Studio.',
318
- '2. Go to **Tools**.',
319
- '3. Select **Add a tool** > **New tool** > **Model Context Protocol**.',
320
- '4. Enter:',
321
- ` - Server name: \`${mcp.displayName}\``,
322
- ` - Server description: \`${mcp.description}\``,
323
- ` - Server URL: \`${mcp.publicUrl}\``,
324
- ' - Authentication: `None`',
325
- '5. Create the tool, add the MCP tools to the agent, then test with: `Search CDP Index for MCP setup.`',
326
- '',
327
- 'Teams / Microsoft 365 Copilot agent path:',
328
- '',
329
- '1. Use Microsoft 365 Agents Toolkit in VS Code.',
330
- '2. Create a new declarative agent.',
331
- '3. Add an action and choose **Start with an MCP server**.',
332
- `4. Enter \`${mcp.publicUrl}\`.`,
333
- '5. Select the tools to expose, usually:',
334
- list(mcp.tools),
335
- '6. Test locally through Microsoft 365 Copilot, then package and publish the agent to Teams/Microsoft 365 through the tenant approval flow.',
336
- '',
337
- mcp.protectedUrl ? `Do not use \`${mcp.protectedUrl}\` for Copilot or Teams unless the Microsoft connector is configured with Cloudflare Access service-token headers. Without that, Microsoft clients receive the Cloudflare Access login redirect instead of MCP JSON-RPC.` : undefined,
338
- '',
339
- ...localRepositorySection(install.repository, mcp),
340
- '',
341
- ...sourceSection(Array.isArray(install.sources) ? install.sources : defaultSources),
342
- '',
343
- ];
344
- return `${lines.filter((line) => line !== undefined).join('\n').replace(/\n{3,}/g, '\n\n')}\n`;
345
- }
1
+ import { installPageLink, installPageMarkdownPath, renderInstallPage, writeInstallPage as writeSharedInstallPage, } from '@raggle-ai/kennel-knowledge/install-page';
2
+ export { installPageLink, installPageMarkdownPath, renderInstallPage, };
346
3
  export function writeInstallPage(root, config) {
347
- const source = renderInstallPage(config);
348
- if (!source)
349
- return undefined;
350
- const resolvedRoot = resolve(root);
351
- const target = resolve(resolvedRoot, installPagePath(config));
352
- if (target !== resolvedRoot && !target.startsWith(`${resolvedRoot}/`)) {
353
- throw new Error(`install.path must resolve inside project root: ${relative(resolvedRoot, target)}`);
354
- }
355
- if (existsSync(target) && readFileSync(target, 'utf8') === source)
356
- return target;
357
- mkdirSync(dirname(target), { recursive: true });
358
- writeFileSync(target, source);
359
- return target;
4
+ return writeSharedInstallPage(config, root);
360
5
  }
@@ -27,7 +27,7 @@ export function notificationEmailConfig(config) {
27
27
  return email;
28
28
  }
29
29
  function workerSource(workerPath, configPath) {
30
- return `import { createCloudflareEmailWorker } from '@raggle-ai/kennel-mcp/notifications/email-worker';
30
+ return `import { createCloudflareEmailWorker } from '@raggle-ai/kennel/mcp/notifications/email-worker';
31
31
  import indexConfig from '${relativeImport(dirname(workerPath), configPath)}';
32
32
 
33
33
  export default createCloudflareEmailWorker(indexConfig);
@@ -4,7 +4,7 @@ import { readJson, readJsonc, writeJson } from '../shared/json.js';
4
4
  import { isObject } from '../shared/object.js';
5
5
  import { displayPath } from '../shared/path.js';
6
6
  import { projectConfigPath } from '../shared/project-config.js';
7
- import { displayBasePath, vitepressOutputDirectory } from '../site/vitepress.js';
7
+ import { displayBasePath, rendererDeploymentDescriptor } from '../site/renderers.js';
8
8
  import { publishAccountId, publishAuthDefaults, publishAuthWithWranglerFallbacks, publicationAdminMcp, publishHostname, publishHostnames, publishWorkerName, publicationConfig, publicationDomains, } from './config.js';
9
9
  import { splitPublishArgs } from './options.js';
10
10
  export const CLOUDFLARE_WORKER_MAIN = '.kennel/generated/cloudflare/email-code-worker.js';
@@ -26,6 +26,9 @@ export function publishInitCloudflare(input) {
26
26
  const configPath = projectConfigPath(options.targetPath, options.configFile);
27
27
  const root = dirname(configPath);
28
28
  const config = readJson(configPath);
29
+ const packagePath = join(root, 'package.json');
30
+ const pkg = existsSync(packagePath) ? readJson(packagePath) : undefined;
31
+ const deployment = rendererDeploymentDescriptor(root, pkg);
29
32
  const publication = publicationConfig(config);
30
33
  const adminMcp = publicationAdminMcp(config);
31
34
  const domains = publicationDomains(config);
@@ -63,7 +66,7 @@ export function publishInitCloudflare(input) {
63
66
  wrangler.compatibility_date = wrangler.compatibility_date ?? new Date().toISOString().slice(0, 10);
64
67
  wrangler.assets = {
65
68
  binding: 'ASSETS',
66
- directory: relative(dirname(wranglerPath), join(root, vitepressOutputDirectory(root))),
69
+ directory: relative(dirname(wranglerPath), join(root, deployment.outputDirectory)),
67
70
  run_worker_first: true,
68
71
  };
69
72
  if (hostnames.length > 0) {
@@ -1,5 +1,5 @@
1
1
  import type { ChildCommand } from '../shared/process.js';
2
- import type { PackageJson } from '../site/vitepress.js';
2
+ import { type PackageJson } from '../site/renderers.js';
3
3
  import type { PublishOptions } from './options.js';
4
4
  export type DopplerTokenCommand = {
5
5
  command: string;
@@ -1,7 +1,8 @@
1
1
  import { join } from 'node:path';
2
2
  import { isObject } from '../shared/object.js';
3
- import { localBin, packageManagerFor, runArgsFor } from '../shared/package-manager.js';
3
+ import { localBin } from '../shared/package-manager.js';
4
4
  import { normalizeHostname, slugifyHostname } from '../shared/path.js';
5
+ import { rendererBuildCommand } from '../site/renderers.js';
5
6
  export function publicationConfig(config) {
6
7
  if (isObject(config.publication))
7
8
  return config.publication;
@@ -156,20 +157,7 @@ export function dopplerTokenCommand(config) {
156
157
  return { command: 'doppler', args };
157
158
  }
158
159
  export function publishBuildCommand(root, pkg) {
159
- const scripts = pkg.scripts ?? {};
160
- const packageManager = packageManagerFor(root);
161
- if (typeof scripts['docs:build'] === 'string') {
162
- return { command: packageManager, args: runArgsFor(packageManager, 'docs:build', []) };
163
- }
164
- if (typeof scripts.build === 'string') {
165
- return { command: packageManager, args: runArgsFor(packageManager, 'build', []) };
166
- }
167
- const vitepress = localBin(root, 'vitepress');
168
- if (vitepress)
169
- return { command: vitepress, args: ['build', '.'] };
170
- if (packageManager === 'npm')
171
- return { command: packageManager, args: ['exec', '--', 'vitepress', 'build', '.'] };
172
- return { command: packageManager, args: ['x', 'vitepress', 'build', '.'] };
160
+ return rendererBuildCommand(root, pkg);
173
161
  }
174
162
  export function publishWranglerCommand(root) {
175
163
  const wrangler = localBin(root, 'wrangler');
@@ -0,0 +1,9 @@
1
+ export declare const VERCEL_CONFIG = "vercel.json";
2
+ export type VercelSetup = {
3
+ root: string;
4
+ configPath: string;
5
+ config: any;
6
+ vercelPath: string;
7
+ hostname: string;
8
+ };
9
+ export declare function publishInitVercel(input: string[]): VercelSetup;
@@ -0,0 +1,52 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { readJson, writeJson } from '../shared/json.js';
4
+ import { displayPath } from '../shared/path.js';
5
+ import { commandLine } from '../shared/process.js';
6
+ import { projectConfigPath } from '../shared/project-config.js';
7
+ import { rendererDeploymentDescriptor } from '../site/renderers.js';
8
+ import { packageJsonPath, publicationConfig, publicationDomains, publishBuildCommand, publishHostname, publishProjectName, } from './config.js';
9
+ import { splitPublishArgs } from './options.js';
10
+ export const VERCEL_CONFIG = 'vercel.json';
11
+ function vercelBuildCommand(root, pkg) {
12
+ return commandLine(publishBuildCommand(root, pkg).command, publishBuildCommand(root, pkg).args).replace(/\s--$/u, '');
13
+ }
14
+ export function publishInitVercel(input) {
15
+ const options = splitPublishArgs(input);
16
+ const configPath = projectConfigPath(options.targetPath, options.configFile);
17
+ const root = dirname(configPath);
18
+ const config = readJson(configPath);
19
+ const packagePath = packageJsonPath(root);
20
+ if (!existsSync(packagePath))
21
+ throw new Error(`No package.json found next to ${configPath}`);
22
+ const pkg = readJson(packagePath);
23
+ const deployment = rendererDeploymentDescriptor(root, pkg);
24
+ const publication = publicationConfig(config);
25
+ const domains = publicationDomains(config);
26
+ const hostname = publishHostname(config, options);
27
+ config.publication = {
28
+ ...publication,
29
+ provider: 'vercel',
30
+ projectName: typeof publication.projectName === 'string' && publication.projectName.trim()
31
+ ? publication.projectName.trim()
32
+ : publishProjectName(config, root),
33
+ domains: {
34
+ ...domains,
35
+ ...(hostname ? { primary: hostname } : {}),
36
+ },
37
+ };
38
+ delete config.publish;
39
+ writeJson(configPath, config);
40
+ const vercelPath = join(root, VERCEL_CONFIG);
41
+ const vercel = existsSync(vercelPath) ? readJson(vercelPath) : {};
42
+ vercel.$schema = vercel.$schema ?? 'https://openapi.vercel.sh/vercel.json';
43
+ vercel.buildCommand = vercelBuildCommand(root, pkg);
44
+ vercel.cleanUrls = deployment.cleanUrls;
45
+ vercel.outputDirectory = deployment.outputDirectory;
46
+ writeJson(vercelPath, vercel);
47
+ console.log(`ok ${displayPath(configPath)}`);
48
+ console.log(`ok ${displayPath(vercelPath)}`);
49
+ if (!hostname)
50
+ console.log('optional: set publication.domains.primary or pass --hostname to record a production hostname');
51
+ return { root, configPath, config, vercelPath, hostname };
52
+ }
package/dist/site/dev.js CHANGED
@@ -5,7 +5,7 @@ import { readJson } from '../shared/json.js';
5
5
  import { normalizeHostname, slugifyHostname } from '../shared/path.js';
6
6
  import { DEFAULT_PROJECT_CONFIG, projectConfigPath } from '../shared/project-config.js';
7
7
  import { commandLine } from '../shared/process.js';
8
- import { buildScriptDevCommand, buildVitePressDevCommand, detectedPlatform, displayBasePath, } from './vitepress.js';
8
+ import { detectedPlatform, displayBasePath, rendererDevCommand, } from './renderers.js';
9
9
  export function splitDevArgs(input) {
10
10
  const options = {
11
11
  targetPath: '.',
@@ -113,9 +113,7 @@ export function dev(input) {
113
113
  const script = options.script ?? devScript(config);
114
114
  const platform = script ? 'script' : detectedPlatform(root, pkg);
115
115
  const toolRoot = devToolRoot(configRoot, config) ?? root;
116
- const child = platform === 'vitepress'
117
- ? buildVitePressDevCommand(root, options, scriptArgs, toolRoot)
118
- : buildScriptDevCommand(root, pkg, { script }, scriptArgs);
116
+ const child = rendererDevCommand(root, pkg, platform, { script }, scriptArgs, toolRoot);
119
117
  console.log(commandLine(child.command, child.args));
120
118
  console.log(`http://${host}:${options.port}${displayBasePath(config)}`);
121
119
  if (options.dryRun)
@@ -0,0 +1,8 @@
1
+ export declare function extensionInstall(): Promise<void>;
2
+ export type PreviewTarget = {
3
+ file?: string;
4
+ root: string;
5
+ };
6
+ export declare function previewTarget(url: string): Promise<PreviewTarget>;
7
+ export declare function previewUrl(port: number, target: PreviewTarget): string;
8
+ export declare function extensionHost(): void;
@@ -0,0 +1,114 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { chmod, mkdir, stat, writeFile } from 'node:fs/promises';
3
+ import { createServer } from 'node:net';
4
+ import { homedir } from 'node:os';
5
+ import { basename, dirname, extname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { setTimeout } from 'node:timers/promises';
8
+ const id = 'popoljjjgainhgpkcdhmneegopeibgom';
9
+ const name = 'com.raggle.kennel';
10
+ export async function extensionInstall() {
11
+ const home = homedir();
12
+ const directory = join(home, '.kennel', 'native');
13
+ const host = join(directory, 'kennel-extension-host');
14
+ const script = resolve(dirname(fileURLToPath(import.meta.url)), '../../bin/kennel-native.mjs');
15
+ const manifests = [
16
+ join(home, 'Library/Application Support/Google/Chrome/NativeMessagingHosts'),
17
+ join(home, 'Library/Application Support/Chromium/NativeMessagingHosts'),
18
+ ];
19
+ await mkdir(directory, { recursive: true });
20
+ await writeFile(host, `#!/bin/sh\nexec "${process.execPath}" "${script}"\n`);
21
+ await chmod(host, 0o755);
22
+ for (const folder of manifests) {
23
+ await mkdir(folder, { recursive: true });
24
+ await writeFile(join(folder, `${name}.json`), JSON.stringify({
25
+ name,
26
+ description: 'Open local files in the Kennel folder viewer.',
27
+ path: host,
28
+ type: 'stdio',
29
+ allowed_origins: [`chrome-extension://${id}/`],
30
+ }, null, 2));
31
+ }
32
+ console.log(`Installed the Kennel Chrome bridge for ${id}`);
33
+ }
34
+ async function available(port) {
35
+ return new Promise((done) => {
36
+ const server = createServer();
37
+ server.once('error', () => done(false));
38
+ server.listen(port, '127.0.0.1', () => server.close(() => done(true)));
39
+ });
40
+ }
41
+ async function current(port) {
42
+ const response = await fetch(`http://127.0.0.1:${port}/api/documents`).catch(() => undefined);
43
+ if (!response?.ok)
44
+ return undefined;
45
+ const body = (await response.json());
46
+ return typeof body.root === 'string' ? body.root : undefined;
47
+ }
48
+ export async function previewTarget(url) {
49
+ if (!url.startsWith('file:'))
50
+ throw new Error('Kennel accepts a local file or folder URL');
51
+ const path = fileURLToPath(url);
52
+ const details = await stat(path);
53
+ if (details.isDirectory())
54
+ return { root: resolve(path) };
55
+ if (!details.isFile())
56
+ throw new Error('Kennel accepts a local file or folder');
57
+ if (!['.htm', '.html', '.markdown', '.md', '.text', '.txt'].includes(extname(path).toLowerCase())) {
58
+ throw new Error('Kennel accepts Markdown, HTML, or text files');
59
+ }
60
+ return { file: basename(path), root: dirname(path) };
61
+ }
62
+ export function previewUrl(port, target) {
63
+ const url = new URL(`http://127.0.0.1:${port}/`);
64
+ if (target.file)
65
+ url.searchParams.set('document', target.file);
66
+ return url.href;
67
+ }
68
+ async function open(url) {
69
+ const target = await previewTarget(url);
70
+ for (let port = 5173; port <= 5199; port += 1) {
71
+ if ((await current(port)) === target.root) {
72
+ return previewUrl(port, target);
73
+ }
74
+ if (!(await available(port)))
75
+ continue;
76
+ const cli = resolve(dirname(fileURLToPath(import.meta.url)), '../../bin/kennel.mjs');
77
+ spawn(process.execPath, [cli, target.root, '--port', String(port)], {
78
+ detached: true,
79
+ stdio: 'ignore',
80
+ }).unref();
81
+ for (let attempt = 0; attempt < 50; attempt += 1) {
82
+ await setTimeout(100);
83
+ if ((await current(port)) === target.root) {
84
+ return previewUrl(port, target);
85
+ }
86
+ }
87
+ throw new Error('Kennel could not start the folder viewer');
88
+ }
89
+ throw new Error('Kennel could not find a free port');
90
+ }
91
+ function send(value) {
92
+ const body = Buffer.from(JSON.stringify(value));
93
+ const header = Buffer.alloc(4);
94
+ header.writeUInt32LE(body.length);
95
+ process.stdout.write(Buffer.concat([header, body]));
96
+ }
97
+ export function extensionHost() {
98
+ let input = Buffer.alloc(0);
99
+ process.stdin.on('data', (chunk) => {
100
+ input = Buffer.concat([input, chunk]);
101
+ while (input.length >= 4) {
102
+ const length = input.readUInt32LE(0);
103
+ if (input.length < length + 4)
104
+ return;
105
+ const message = JSON.parse(input.subarray(4, length + 4).toString());
106
+ input = input.subarray(length + 4);
107
+ if (typeof message.url !== 'string') {
108
+ send({ error: 'The file URL is missing' });
109
+ continue;
110
+ }
111
+ void open(message.url).then((url) => send({ url }), (error) => send({ error: error instanceof Error ? error.message : 'Kennel failed' }));
112
+ }
113
+ });
114
+ }