@doxbrix/doxloop 0.1.4 → 0.2.0

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 (357) hide show
  1. package/CHANGELOG.md +1153 -0
  2. package/README.md +422 -226
  3. package/assets/doxbrix-preview.css +77 -15
  4. package/contracts/agent-events-v1.schema.json +1 -0
  5. package/contracts/coverage-v1.schema.json +59 -0
  6. package/contracts/drift-v1.schema.json +1 -0
  7. package/contracts/evaluation-v1.schema.json +62 -0
  8. package/contracts/quality-codes-v1.json +1 -0
  9. package/contracts/quality-report-v1.schema.json +1 -0
  10. package/contracts/validation-v1.schema.json +1 -0
  11. package/dist/agent-failure.d.ts +36 -0
  12. package/dist/agent-failure.js +98 -0
  13. package/dist/agent-isolation.d.ts +17 -0
  14. package/dist/agent-isolation.js +94 -0
  15. package/dist/agent-log.d.ts +197 -0
  16. package/dist/agent-log.js +1094 -0
  17. package/dist/agent-process.d.ts +63 -0
  18. package/dist/agent-process.js +128 -0
  19. package/dist/agent-reply.d.ts +47 -0
  20. package/dist/agent-reply.js +315 -0
  21. package/dist/agents.d.ts +40 -4
  22. package/dist/agents.js +115 -11
  23. package/dist/api-coverage.d.ts +11 -0
  24. package/dist/api-coverage.js +72 -0
  25. package/dist/application-probe.d.ts +28 -0
  26. package/dist/application-probe.js +93 -0
  27. package/dist/application-url.d.ts +14 -0
  28. package/dist/application-url.js +26 -0
  29. package/dist/args.js +15 -0
  30. package/dist/artifact-deploy.d.ts +9 -0
  31. package/dist/artifact-deploy.js +28 -8
  32. package/dist/assets.d.ts +72 -0
  33. package/dist/assets.js +382 -0
  34. package/dist/auth.d.ts +10 -0
  35. package/dist/auth.js +38 -9
  36. package/dist/author.d.ts +206 -4
  37. package/dist/author.js +1650 -52
  38. package/dist/authoring-batches.d.ts +190 -0
  39. package/dist/authoring-batches.js +489 -0
  40. package/dist/authoring-postpass.d.ts +79 -0
  41. package/dist/authoring-postpass.js +1289 -0
  42. package/dist/authoring-progress.d.ts +95 -0
  43. package/dist/authoring-progress.js +269 -0
  44. package/dist/autosync.d.ts +56 -0
  45. package/dist/autosync.js +442 -0
  46. package/dist/batch-artifacts.d.ts +81 -0
  47. package/dist/batch-artifacts.js +412 -0
  48. package/dist/batch-limits.d.ts +37 -0
  49. package/dist/batch-limits.js +80 -0
  50. package/dist/branding.d.ts +41 -0
  51. package/dist/branding.js +229 -0
  52. package/dist/bundle-upload.d.ts +26 -0
  53. package/dist/bundle-upload.js +99 -0
  54. package/dist/capture-auth.d.ts +95 -0
  55. package/dist/capture-auth.js +204 -0
  56. package/dist/capture.d.ts +48 -0
  57. package/dist/capture.js +20 -1
  58. package/dist/cli.js +839 -32
  59. package/dist/content-links.d.ts +10 -0
  60. package/dist/content-links.js +49 -0
  61. package/dist/contract-validation.d.ts +4 -0
  62. package/dist/contract-validation.js +25 -0
  63. package/dist/coverage-actions.d.ts +8 -0
  64. package/dist/coverage-actions.js +71 -0
  65. package/dist/coverage-resolutions.d.ts +16 -0
  66. package/dist/coverage-resolutions.js +58 -0
  67. package/dist/db.d.ts +37 -0
  68. package/dist/db.js +288 -0
  69. package/dist/demo.d.ts +12 -0
  70. package/dist/demo.js +122 -0
  71. package/dist/deploy-credentials.d.ts +7 -0
  72. package/dist/deploy-credentials.js +76 -0
  73. package/dist/deploy-targets/github-pages.d.ts +3 -0
  74. package/dist/deploy-targets/github-pages.js +100 -0
  75. package/dist/deploy-targets/index.d.ts +8 -0
  76. package/dist/deploy-targets/index.js +78 -0
  77. package/dist/deploy-targets/netlify.d.ts +3 -0
  78. package/dist/deploy-targets/netlify.js +49 -0
  79. package/dist/deploy-targets/types.d.ts +32 -0
  80. package/dist/deploy-targets/types.js +2 -0
  81. package/dist/deploy-targets/vercel.d.ts +3 -0
  82. package/dist/deploy-targets/vercel.js +95 -0
  83. package/dist/deploy.d.ts +9 -0
  84. package/dist/deploy.js +87 -13
  85. package/dist/deterministic-capture.d.ts +90 -0
  86. package/dist/deterministic-capture.js +435 -0
  87. package/dist/direct-edit.d.ts +40 -0
  88. package/dist/direct-edit.js +159 -0
  89. package/dist/docs-crawl.d.ts +86 -0
  90. package/dist/docs-crawl.js +536 -0
  91. package/dist/docs-site.d.ts +49 -0
  92. package/dist/docs-site.js +233 -0
  93. package/dist/doctor.js +8 -0
  94. package/dist/documentation-collections.d.ts +23 -0
  95. package/dist/documentation-collections.js +191 -0
  96. package/dist/documentation-plan.d.ts +253 -0
  97. package/dist/documentation-plan.js +2563 -0
  98. package/dist/doxbrix-build.d.ts +19 -0
  99. package/dist/doxbrix-build.js +167 -0
  100. package/dist/doxbrix-markdown.d.ts +9 -0
  101. package/dist/doxbrix-markdown.js +115 -14
  102. package/dist/drift.d.ts +10 -0
  103. package/dist/drift.js +164 -0
  104. package/dist/evaluation.d.ts +46 -0
  105. package/dist/evaluation.js +113 -0
  106. package/dist/evidence-pack.d.ts +47 -0
  107. package/dist/evidence-pack.js +358 -0
  108. package/dist/evidence.d.ts +25 -0
  109. package/dist/evidence.js +175 -0
  110. package/dist/fs.d.ts +8 -2
  111. package/dist/fs.js +41 -11
  112. package/dist/generator-api.d.ts +89 -0
  113. package/dist/generator-preflight.d.ts +27 -0
  114. package/dist/generator-preflight.js +105 -0
  115. package/dist/generator-runtime.d.ts +7 -0
  116. package/dist/generator-runtime.js +17 -1
  117. package/dist/generators.d.ts +24 -2
  118. package/dist/generators.js +54 -1
  119. package/dist/git-delivery.d.ts +17 -0
  120. package/dist/git-delivery.js +123 -0
  121. package/dist/globs.d.ts +16 -0
  122. package/dist/globs.js +65 -0
  123. package/dist/glossary.d.ts +26 -0
  124. package/dist/glossary.js +179 -0
  125. package/dist/history.d.ts +106 -0
  126. package/dist/history.js +600 -0
  127. package/dist/html-markdown.d.ts +46 -0
  128. package/dist/html-markdown.js +423 -0
  129. package/dist/interactive.js +16 -15
  130. package/dist/job-events.d.ts +74 -0
  131. package/dist/job-events.js +377 -0
  132. package/dist/keep-awake.d.ts +50 -0
  133. package/dist/keep-awake.js +123 -0
  134. package/dist/local-source-snapshot.d.ts +20 -0
  135. package/dist/local-source-snapshot.js +61 -0
  136. package/dist/mintlify-detect.d.ts +3 -0
  137. package/dist/mintlify-detect.js +18 -0
  138. package/dist/mintlify-import.d.ts +75 -0
  139. package/dist/mintlify-import.js +190 -0
  140. package/dist/navigation.d.ts +98 -0
  141. package/dist/navigation.js +310 -0
  142. package/dist/openapi.d.ts +60 -0
  143. package/dist/openapi.js +439 -0
  144. package/dist/page-editor-bridge.d.ts +3 -0
  145. package/dist/page-editor-bridge.js +109 -0
  146. package/dist/page-editor-preview.d.ts +10 -0
  147. package/dist/page-editor-preview.js +55 -0
  148. package/dist/page-extension.d.ts +9 -0
  149. package/dist/page-extension.js +15 -0
  150. package/dist/page-metadata.d.ts +28 -0
  151. package/dist/page-metadata.js +166 -0
  152. package/dist/page-operations.d.ts +34 -0
  153. package/dist/page-operations.js +215 -0
  154. package/dist/page-routes.d.ts +4 -0
  155. package/dist/page-routes.js +61 -0
  156. package/dist/pages.d.ts +20 -0
  157. package/dist/pages.js +184 -0
  158. package/dist/plan-generator.d.ts +3 -0
  159. package/dist/plan-generator.js +21 -0
  160. package/dist/plan-navigation.d.ts +11 -0
  161. package/dist/plan-navigation.js +30 -0
  162. package/dist/planning-captures.d.ts +20 -0
  163. package/dist/planning-captures.js +143 -0
  164. package/dist/planning-research.d.ts +135 -0
  165. package/dist/planning-research.js +472 -0
  166. package/dist/planning-triage.d.ts +23 -0
  167. package/dist/planning-triage.js +131 -0
  168. package/dist/preview.d.ts +24 -0
  169. package/dist/preview.js +280 -29
  170. package/dist/project-detect.d.ts +36 -0
  171. package/dist/project-detect.js +251 -0
  172. package/dist/project-import.d.ts +54 -0
  173. package/dist/project-import.js +157 -0
  174. package/dist/project-lock.d.ts +6 -0
  175. package/dist/project-lock.js +96 -0
  176. package/dist/project-registry.d.ts +25 -0
  177. package/dist/project-registry.js +79 -0
  178. package/dist/project.d.ts +35 -3
  179. package/dist/project.js +388 -38
  180. package/dist/prompts.d.ts +9 -0
  181. package/dist/prompts.js +33 -4
  182. package/dist/proposal-replay.d.ts +32 -0
  183. package/dist/proposal-replay.js +99 -0
  184. package/dist/quality-claims.d.ts +8 -0
  185. package/dist/quality-claims.js +168 -0
  186. package/dist/quality-config.d.ts +5 -0
  187. package/dist/quality-config.js +84 -0
  188. package/dist/quality-contract.d.ts +37 -0
  189. package/dist/quality-contract.js +46 -0
  190. package/dist/quality-examples.d.ts +4 -0
  191. package/dist/quality-examples.js +233 -0
  192. package/dist/quality-gates.d.ts +16 -0
  193. package/dist/quality-gates.js +192 -0
  194. package/dist/quality-links.d.ts +7 -0
  195. package/dist/quality-links.js +149 -0
  196. package/dist/quality-lint.d.ts +6 -0
  197. package/dist/quality-lint.js +124 -0
  198. package/dist/quality-rendered.d.ts +11 -0
  199. package/dist/quality-rendered.js +222 -0
  200. package/dist/quality-schema.d.ts +3 -0
  201. package/dist/quality-schema.js +71 -0
  202. package/dist/release-notes.d.ts +44 -0
  203. package/dist/release-notes.js +183 -0
  204. package/dist/remote-monitor.d.ts +16 -0
  205. package/dist/remote-monitor.js +74 -0
  206. package/dist/remote-source.d.ts +34 -0
  207. package/dist/remote-source.js +426 -0
  208. package/dist/review-diff.d.ts +82 -0
  209. package/dist/review-diff.js +400 -0
  210. package/dist/review-learning.d.ts +11 -0
  211. package/dist/review-learning.js +60 -0
  212. package/dist/review-render.d.ts +38 -0
  213. package/dist/review-render.js +224 -0
  214. package/dist/review-report.d.ts +9 -0
  215. package/dist/review-report.js +89 -0
  216. package/dist/review-ui.d.ts +14 -0
  217. package/dist/review-ui.js +1248 -0
  218. package/dist/schedule.d.ts +78 -0
  219. package/dist/schedule.js +480 -0
  220. package/dist/screen-capture-provider.d.ts +52 -0
  221. package/dist/screen-capture-provider.js +218 -0
  222. package/dist/screenshot-workflow.d.ts +167 -0
  223. package/dist/screenshot-workflow.js +1237 -0
  224. package/dist/settings.d.ts +1 -1
  225. package/dist/settings.js +95 -7
  226. package/dist/site-export.d.ts +18 -0
  227. package/dist/site-export.js +87 -0
  228. package/dist/source-connectors.d.ts +33 -0
  229. package/dist/source-connectors.js +268 -0
  230. package/dist/source-discovery.d.ts +132 -0
  231. package/dist/source-discovery.js +823 -0
  232. package/dist/source-intelligence.d.ts +9 -0
  233. package/dist/source-intelligence.js +306 -0
  234. package/dist/sync-review.d.ts +28 -0
  235. package/dist/sync-review.js +264 -0
  236. package/dist/sync-runs.d.ts +192 -0
  237. package/dist/sync-runs.js +2244 -0
  238. package/dist/sync.d.ts +35 -0
  239. package/dist/sync.js +298 -32
  240. package/dist/text-diff.d.ts +9 -0
  241. package/dist/text-diff.js +59 -0
  242. package/dist/types.d.ts +946 -1
  243. package/dist/ui/assets/doxloop-logo-light-De7Nx7j7.png +0 -0
  244. package/dist/ui/assets/index-BHBYU2aG.css +1 -0
  245. package/dist/ui/assets/index-Cq3RPQiC.js +33 -0
  246. package/dist/ui/index.html +18 -0
  247. package/dist/ui-server.d.ts +83 -0
  248. package/dist/ui-server.js +3532 -0
  249. package/dist/usage-budget.d.ts +28 -0
  250. package/dist/usage-budget.js +90 -0
  251. package/dist/validation.d.ts +25 -1
  252. package/dist/validation.js +312 -26
  253. package/dist/workspace-tools.d.ts +54 -0
  254. package/dist/workspace-tools.js +123 -0
  255. package/docs/agent-compatibility.md +50 -28
  256. package/docs/ci-and-automation.md +105 -66
  257. package/docs/doxbrix-http-api.md +8 -1
  258. package/docs/existing-documentation.md +80 -0
  259. package/docs/generation-performance.md +108 -0
  260. package/docs/generator-authoring.md +68 -5
  261. package/docs/generator-selection.md +50 -13
  262. package/docs/mintlify-import.md +71 -0
  263. package/docs/openapi-security.md +25 -0
  264. package/docs/project-format.md +331 -36
  265. package/docs/release-quality.md +158 -0
  266. package/docs/releasing.md +72 -0
  267. package/docs/review-workflows.md +51 -0
  268. package/docs/security-model.md +102 -39
  269. package/docs/troubleshooting.md +226 -93
  270. package/package.json +46 -15
  271. package/scripts/test-auto-screenshot.mjs +172 -0
  272. package/skills/doxloop-authoring/SKILL.md +242 -361
  273. package/skills/doxloop-authoring/references/existing-documentation.md +94 -0
  274. package/skills/doxloop-authoring/references/navigation-architecture.md +18 -7
  275. package/skills/doxloop-authoring/references/page-depth.md +169 -0
  276. package/skills/doxloop-authoring/references/project-format.md +106 -7
  277. package/skills/doxloop-authoring/references/quality.md +10 -0
  278. package/skills/doxloop-authoring/references/screenshot-manifest.md +113 -0
  279. package/skills/doxloop-authoring/references/screenshots.md +149 -235
  280. package/skills/doxloop-authoring/references/workflows.md +76 -0
  281. package/skills/doxloop-doxbrix/SKILL.md +38 -22
  282. package/skills/doxloop-doxbrix/references/api-endpoints.md +15 -14
  283. package/skills/doxloop-doxbrix/references/components.md +30 -3
  284. package/skills/doxloop-doxbrix/references/manifest.md +3 -2
  285. package/vendor/doxbrix-import/LICENSE +202 -0
  286. package/vendor/doxbrix-import/README.md +17 -0
  287. package/vendor/doxbrix-import/UPSTREAM.json +47 -0
  288. package/vendor/doxbrix-import/dist/docs/frontmatter.d.ts +13 -0
  289. package/vendor/doxbrix-import/dist/docs/frontmatter.js +83 -0
  290. package/vendor/doxbrix-import/dist/docs/import.d.ts +21 -0
  291. package/vendor/doxbrix-import/dist/docs/import.js +147 -0
  292. package/vendor/doxbrix-import/dist/docs/manifest.d.ts +163 -0
  293. package/vendor/doxbrix-import/dist/docs/manifest.js +64 -0
  294. package/vendor/doxbrix-import/dist/docs/project.d.ts +25 -0
  295. package/vendor/doxbrix-import/dist/docs/project.js +77 -0
  296. package/vendor/doxbrix-import/dist/docs/starter.d.ts +4 -0
  297. package/vendor/doxbrix-import/dist/docs/starter.js +11 -0
  298. package/vendor/doxbrix-import/dist/importer.d.ts +227 -0
  299. package/vendor/doxbrix-import/dist/importer.js +1567 -0
  300. package/vendor/doxbrix-import/dist/mintlify-openapi.d.ts +37 -0
  301. package/vendor/doxbrix-import/dist/mintlify-openapi.js +305 -0
  302. package/vendor/doxbrix-import/dist/safe-path.d.ts +9 -0
  303. package/vendor/doxbrix-import/dist/safe-path.js +47 -0
  304. package/dist/agents.d.ts.map +0 -1
  305. package/dist/agents.js.map +0 -1
  306. package/dist/args.d.ts.map +0 -1
  307. package/dist/args.js.map +0 -1
  308. package/dist/artifact-deploy.d.ts.map +0 -1
  309. package/dist/artifact-deploy.js.map +0 -1
  310. package/dist/auth.d.ts.map +0 -1
  311. package/dist/auth.js.map +0 -1
  312. package/dist/author.d.ts.map +0 -1
  313. package/dist/author.js.map +0 -1
  314. package/dist/capture.d.ts.map +0 -1
  315. package/dist/capture.js.map +0 -1
  316. package/dist/cli.d.ts.map +0 -1
  317. package/dist/cli.js.map +0 -1
  318. package/dist/deploy.d.ts.map +0 -1
  319. package/dist/deploy.js.map +0 -1
  320. package/dist/deployment-visibility.d.ts.map +0 -1
  321. package/dist/deployment-visibility.js.map +0 -1
  322. package/dist/doctor.d.ts.map +0 -1
  323. package/dist/doctor.js.map +0 -1
  324. package/dist/doxbrix-markdown.d.ts.map +0 -1
  325. package/dist/doxbrix-markdown.js.map +0 -1
  326. package/dist/errors.d.ts.map +0 -1
  327. package/dist/errors.js.map +0 -1
  328. package/dist/fs.d.ts.map +0 -1
  329. package/dist/fs.js.map +0 -1
  330. package/dist/generator-api.d.ts.map +0 -1
  331. package/dist/generator-api.js.map +0 -1
  332. package/dist/generator-manager.d.ts.map +0 -1
  333. package/dist/generator-manager.js.map +0 -1
  334. package/dist/generator-runtime.d.ts.map +0 -1
  335. package/dist/generator-runtime.js.map +0 -1
  336. package/dist/generators.d.ts.map +0 -1
  337. package/dist/generators.js.map +0 -1
  338. package/dist/interactive.d.ts.map +0 -1
  339. package/dist/interactive.js.map +0 -1
  340. package/dist/preview.d.ts.map +0 -1
  341. package/dist/preview.js.map +0 -1
  342. package/dist/progress.d.ts.map +0 -1
  343. package/dist/progress.js.map +0 -1
  344. package/dist/project.d.ts.map +0 -1
  345. package/dist/project.js.map +0 -1
  346. package/dist/prompts.d.ts.map +0 -1
  347. package/dist/prompts.js.map +0 -1
  348. package/dist/settings.d.ts.map +0 -1
  349. package/dist/settings.js.map +0 -1
  350. package/dist/sync.d.ts.map +0 -1
  351. package/dist/sync.js.map +0 -1
  352. package/dist/types.d.ts.map +0 -1
  353. package/dist/types.js.map +0 -1
  354. package/dist/validation.d.ts.map +0 -1
  355. package/dist/validation.js.map +0 -1
  356. package/dist/version.d.ts.map +0 -1
  357. package/dist/version.js.map +0 -1
@@ -0,0 +1,426 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { execFile } from 'node:child_process';
3
+ import { mkdir, mkdtemp, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, isAbsolute, join, posix, relative, resolve } from 'node:path';
6
+ import { promisify } from 'node:util';
7
+ import { unzipSync } from 'fflate';
8
+ import { DoxloopError } from './errors.js';
9
+ import { pathExists } from './fs.js';
10
+ const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024;
11
+ const MAX_EXTRACTED_BYTES = 500 * 1024 * 1024;
12
+ const execFileAsync = promisify(execFile);
13
+ const gitCredentials = new Map();
14
+ const GIT_CREDENTIALS_ENV = 'DOXLOOP_SESSION_GIT_CREDENTIALS';
15
+ /** Validate a provider-neutral Git clone URL without embedding credentials. */
16
+ export function parseGitRepository(value) {
17
+ const input = value.trim();
18
+ if (!input || /[\r\n\0]/.test(input))
19
+ throw new DoxloopError('Enter a Git repository URL.');
20
+ if (/^[^@\s]+@[^:\s]+:.+$/.test(input))
21
+ return input;
22
+ try {
23
+ const url = new URL(input);
24
+ if (!['https:', 'http:', 'ssh:', 'git:', 'file:'].includes(url.protocol) || url.password)
25
+ throw new Error('unsupported');
26
+ return input;
27
+ }
28
+ catch {
29
+ throw new DoxloopError('Enter a Git clone URL from GitHub, GitLab, Azure DevOps, or another Git service.');
30
+ }
31
+ }
32
+ /** Keep private-repository credentials in memory for this Doxloop UI session. */
33
+ export function rememberRemoteCredential(repository, username, secret) {
34
+ const key = credentialKey(repository);
35
+ if (!secret) {
36
+ gitCredentials.delete(key);
37
+ return;
38
+ }
39
+ gitCredentials.set(key, { ...(username?.trim() ? { username: username.trim() } : {}), secret });
40
+ }
41
+ /**
42
+ * Forward UI-session credentials to Doxloop child processes without writing
43
+ * them to the project configuration or placing them on a command line.
44
+ */
45
+ export function remoteCredentialEnvironment() {
46
+ if (gitCredentials.size === 0)
47
+ return {};
48
+ return { [GIT_CREDENTIALS_ENV]: JSON.stringify(Object.fromEntries(gitCredentials)) };
49
+ }
50
+ /** Accept GitHub web/clone URLs or owner/name and return the canonical slug. */
51
+ export function parseGitHubRepository(value) {
52
+ const input = value.trim();
53
+ const slug = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(input)
54
+ ? input
55
+ : githubSlugFromUrl(input);
56
+ if (!slug || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(slug)) {
57
+ throw new DoxloopError('Enter a GitHub repository URL, for example https://github.com/acme/product.');
58
+ }
59
+ return slug.replace(/\.git$/i, '');
60
+ }
61
+ function githubSlugFromUrl(value) {
62
+ const ssh = /^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?$/.exec(value);
63
+ if (ssh)
64
+ return `${ssh[1]}/${ssh[2]}`;
65
+ try {
66
+ const url = new URL(value);
67
+ if (url.protocol !== 'https:' || url.hostname.toLowerCase() !== 'github.com' || url.username || url.password)
68
+ return;
69
+ const parts = url.pathname.replace(/^\/+|\/+$/g, '').split('/');
70
+ if (parts.length !== 2)
71
+ return;
72
+ return `${parts[0]}/${parts[1].replace(/\.git$/i, '')}`;
73
+ }
74
+ catch {
75
+ return;
76
+ }
77
+ }
78
+ export async function listRemoteBranches(remote) {
79
+ if (remote.provider === 'git')
80
+ return listGitBranches(remote);
81
+ const response = await githubRequest(remote, `/repos/${repositoryPath(remote)}/branches?per_page=100`);
82
+ const branches = (await response.json());
83
+ return branches.flatMap((branch) => {
84
+ const name = branch.name;
85
+ const head = branch.commit?.sha;
86
+ return name && head && /^[0-9a-f]{40}$/i.test(head) ? [{ name, head }] : [];
87
+ });
88
+ }
89
+ export async function remoteHead(remote) {
90
+ if (remote.provider === 'git')
91
+ return gitRemoteHead(remote);
92
+ const response = await githubRequest(remote, `/repos/${repositoryPath(remote)}/branches/${encodeURIComponent(remote.branch)}`);
93
+ const branch = (await response.json());
94
+ const sha = branch.commit?.sha;
95
+ if (!sha || !/^[0-9a-f]{40}$/i.test(sha)) {
96
+ throw new DoxloopError(`GitHub returned an invalid head for ${remote.repository}@${remote.branch}.`);
97
+ }
98
+ return sha;
99
+ }
100
+ /** List repository folders without checking out file contents. */
101
+ export async function listRemoteDirectories(remote) {
102
+ if (remote.provider === 'github')
103
+ return listGitHubDirectories(remote);
104
+ const temporary = await mkdtemp(join(tmpdir(), 'doxloop-git-tree-'));
105
+ try {
106
+ await rm(temporary, { recursive: true, force: true });
107
+ await runGit(remote, [
108
+ 'clone', '--depth', '1', '--single-branch', '--branch', remote.branch,
109
+ '--filter=blob:none', '--no-checkout', '--no-tags', '--quiet', remote.repository, temporary,
110
+ ]);
111
+ const output = await runGit(remote, ['-C', temporary, 'ls-tree', '-d', '-r', '--name-only', 'HEAD']);
112
+ return output.split(/\r?\n/).map((directory) => directory.trim()).filter(Boolean).slice(0, 5000);
113
+ }
114
+ finally {
115
+ await rm(temporary, { recursive: true, force: true });
116
+ }
117
+ }
118
+ export async function remoteChangedFiles(remote, baseline, head) {
119
+ if (remote.provider === 'git')
120
+ return gitChangedFiles(remote, baseline, head);
121
+ const response = await githubRequest(remote, `/repos/${repositoryPath(remote)}/compare/${encodeURIComponent(baseline)}...${encodeURIComponent(head)}?per_page=100`);
122
+ const comparison = (await response.json());
123
+ return (comparison.files ?? []).flatMap((file) => {
124
+ if (!file.filename)
125
+ return [];
126
+ const status = file.status === 'added' ? 'A' : file.status === 'removed' ? 'D' : file.status === 'renamed' ? 'R100' : 'M';
127
+ if (status === 'R100' && file.previous_filename)
128
+ return [`${status}\t${file.previous_filename}\t${file.filename}`];
129
+ return [`${status}\t${file.filename}`];
130
+ });
131
+ }
132
+ /** Download a provider snapshot outside the documentation deployment boundary. */
133
+ export async function materializeRemoteSource(root, source, requestedHead) {
134
+ const remote = source.remote;
135
+ if (remote.provider === 'git')
136
+ return materializeGitSource(root, source, requestedHead);
137
+ const head = requestedHead ?? await remoteHead(remote);
138
+ const projectId = createHash('sha256').update(resolve(root)).digest('hex').slice(0, 12);
139
+ const destination = join(dirname(resolve(root)), '.doxloop-sources', projectId, safeName(source.name), head);
140
+ let files = 0;
141
+ if (!(await pathExists(destination))) {
142
+ const parent = dirname(destination);
143
+ await mkdir(parent, { recursive: true });
144
+ const temporary = await mkdtemp(join(parent, '.download-'));
145
+ try {
146
+ const response = await githubRequest(remote, `/repos/${repositoryPath(remote)}/zipball/${encodeURIComponent(head)}`, 'application/vnd.github+json');
147
+ const archive = new Uint8Array(await response.arrayBuffer());
148
+ if (archive.byteLength > MAX_ARCHIVE_BYTES) {
149
+ throw new DoxloopError(`The source archive exceeds the ${MAX_ARCHIVE_BYTES / 1024 / 1024} MB safety limit.`);
150
+ }
151
+ const entries = unzipSync(archive);
152
+ let extracted = 0;
153
+ for (const [rawName, content] of Object.entries(entries)) {
154
+ const normalized = posix.normalize(rawName.replace(/\\/g, '/'));
155
+ const parts = normalized.split('/').filter(Boolean);
156
+ if (parts.length < 2 || normalized.startsWith('/') || parts.includes('..'))
157
+ continue;
158
+ const archiveRelative = parts.slice(1).join('/');
159
+ if (!archiveRelative)
160
+ continue;
161
+ const output = resolve(temporary, archiveRelative);
162
+ if (!output.startsWith(`${resolve(temporary)}/`))
163
+ throw new DoxloopError('GitHub returned an unsafe archive path.');
164
+ if (normalized.endsWith('/')) {
165
+ await mkdir(output, { recursive: true });
166
+ continue;
167
+ }
168
+ extracted += content.byteLength;
169
+ if (extracted > MAX_EXTRACTED_BYTES) {
170
+ throw new DoxloopError(`The expanded source exceeds the ${MAX_EXTRACTED_BYTES / 1024 / 1024} MB safety limit.`);
171
+ }
172
+ await mkdir(dirname(output), { recursive: true });
173
+ await writeFile(output, content);
174
+ files += 1;
175
+ }
176
+ await rename(temporary, destination);
177
+ }
178
+ catch (error) {
179
+ await rm(temporary, { recursive: true, force: true });
180
+ throw error;
181
+ }
182
+ }
183
+ const scoped = remote.subdirectory
184
+ ? resolveRemoteSubdirectory(destination, remote.subdirectory)
185
+ : destination;
186
+ try {
187
+ if (!(await stat(scoped)).isDirectory())
188
+ throw new Error('not a directory');
189
+ }
190
+ catch {
191
+ throw new DoxloopError(`The repository subdirectory does not exist on ${remote.branch}: ${remote.subdirectory}`);
192
+ }
193
+ return { path: scoped, head, files };
194
+ }
195
+ export function changedFilesInRemoteScope(files, subdirectory) {
196
+ if (!subdirectory)
197
+ return files;
198
+ const prefix = `${normalizeSubdirectory(subdirectory)}/`;
199
+ return files.flatMap((entry) => {
200
+ const columns = entry.split('\t');
201
+ const paths = columns.slice(1);
202
+ if (!paths.some((path) => path === prefix.slice(0, -1) || path.startsWith(prefix)))
203
+ return [];
204
+ return [columns.map((column, index) => index === 0 ? column : column.startsWith(prefix) ? column.slice(prefix.length) : column).join('\t')];
205
+ });
206
+ }
207
+ export function portableSourcePath(root, sourcePath) {
208
+ return relative(resolve(root), resolve(sourcePath)).split('\\').join('/');
209
+ }
210
+ function resolveRemoteSubdirectory(root, value) {
211
+ const normalized = normalizeSubdirectory(value);
212
+ const destination = resolve(root, normalized);
213
+ if (isAbsolute(normalized) || destination === resolve(root) || !destination.startsWith(`${resolve(root)}/`)) {
214
+ throw new DoxloopError('Repository subdirectory must be a safe relative directory.');
215
+ }
216
+ return destination;
217
+ }
218
+ function normalizeSubdirectory(value) {
219
+ return posix.normalize(value.trim().replace(/\\/g, '/')).replace(/^\.\//, '').replace(/\/$/, '');
220
+ }
221
+ async function listGitHubDirectories(remote) {
222
+ const head = await remoteHead(remote);
223
+ const response = await githubRequest(remote, `/repos/${repositoryPath(remote)}/git/trees/${encodeURIComponent(head)}?recursive=1`);
224
+ const tree = (await response.json());
225
+ return (tree.tree ?? []).flatMap((entry) => entry.type === 'tree' && entry.path ? [entry.path] : []).slice(0, 5000);
226
+ }
227
+ async function listGitBranches(remote) {
228
+ const output = await runGit(remote, ['ls-remote', '--heads', remote.repository]);
229
+ const branches = output.split(/\r?\n/).flatMap((line) => {
230
+ const match = /^([0-9a-f]{40,64})\s+refs\/heads\/(.+)$/i.exec(line.trim());
231
+ return match ? [{ name: match[2], head: match[1] }] : [];
232
+ });
233
+ const defaultBranch = await gitDefaultBranch(remote);
234
+ return branches.sort((a, b) => a.name === defaultBranch ? -1 : b.name === defaultBranch ? 1 : a.name.localeCompare(b.name));
235
+ }
236
+ async function gitDefaultBranch(remote) {
237
+ try {
238
+ const output = await runGit(remote, ['ls-remote', '--symref', remote.repository, 'HEAD']);
239
+ return /^ref:\s+refs\/heads\/(.+)\s+HEAD$/m.exec(output)?.[1];
240
+ }
241
+ catch {
242
+ return undefined;
243
+ }
244
+ }
245
+ async function gitRemoteHead(remote) {
246
+ const output = await runGit(remote, ['ls-remote', remote.repository, `refs/heads/${remote.branch}`]);
247
+ const match = /^([0-9a-f]{40,64})\s+refs\/heads\/.+$/im.exec(output);
248
+ if (!match)
249
+ throw new DoxloopError(`Branch "${remote.branch}" was not found in the repository.`);
250
+ return match[1];
251
+ }
252
+ async function gitChangedFiles(remote, baseline, head) {
253
+ const temporary = await mkdtemp(join(tmpdir(), 'doxloop-git-diff-'));
254
+ try {
255
+ await runGit(remote, ['clone', '--bare', '--filter=blob:none', '--quiet', remote.repository, temporary]);
256
+ const output = await runGit(remote, ['-C', temporary, 'diff', '--name-status', baseline, head]);
257
+ return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
258
+ }
259
+ finally {
260
+ await rm(temporary, { recursive: true, force: true });
261
+ }
262
+ }
263
+ async function materializeGitSource(root, source, requestedHead) {
264
+ const remote = source.remote;
265
+ const head = requestedHead ?? await gitRemoteHead(remote);
266
+ const projectId = createHash('sha256').update(resolve(root)).digest('hex').slice(0, 12);
267
+ const destination = join(dirname(resolve(root)), '.doxloop-sources', projectId, safeName(source.name), head);
268
+ let files = 0;
269
+ if (!(await pathExists(destination))) {
270
+ const parent = dirname(destination);
271
+ await mkdir(parent, { recursive: true });
272
+ const temporary = await mkdtemp(join(parent, '.clone-'));
273
+ try {
274
+ await rm(temporary, { recursive: true, force: true });
275
+ await runGit(remote, [
276
+ 'clone', '--depth', '1', '--single-branch', '--branch', remote.branch,
277
+ '--no-tags', '--quiet', remote.repository, temporary,
278
+ ]);
279
+ const actualHead = (await runGit(remote, ['-C', temporary, 'rev-parse', 'HEAD'])).trim();
280
+ if (actualHead !== head)
281
+ throw new DoxloopError('The repository changed while Doxloop was preparing it. Connect again to use the latest version.');
282
+ await rm(join(temporary, '.git'), { recursive: true, force: true });
283
+ const measured = await measureDirectory(temporary);
284
+ if (measured.bytes > MAX_EXTRACTED_BYTES) {
285
+ throw new DoxloopError(`The source exceeds the ${MAX_EXTRACTED_BYTES / 1024 / 1024} MB safety limit.`);
286
+ }
287
+ files = measured.files;
288
+ await rename(temporary, destination);
289
+ }
290
+ catch (error) {
291
+ await rm(temporary, { recursive: true, force: true });
292
+ throw error;
293
+ }
294
+ }
295
+ const scoped = remote.subdirectory ? resolveRemoteSubdirectory(destination, remote.subdirectory) : destination;
296
+ try {
297
+ if (!(await stat(scoped)).isDirectory())
298
+ throw new Error('not a directory');
299
+ }
300
+ catch {
301
+ throw new DoxloopError(`The repository subdirectory does not exist on ${remote.branch}: ${remote.subdirectory}`);
302
+ }
303
+ return { path: scoped, head, files };
304
+ }
305
+ async function measureDirectory(root) {
306
+ let files = 0;
307
+ let bytes = 0;
308
+ for (const entry of await readdir(root, { withFileTypes: true })) {
309
+ const path = join(root, entry.name);
310
+ if (entry.isDirectory()) {
311
+ const child = await measureDirectory(path);
312
+ files += child.files;
313
+ bytes += child.bytes;
314
+ }
315
+ else if (entry.isFile()) {
316
+ files += 1;
317
+ bytes += (await stat(path)).size;
318
+ }
319
+ }
320
+ return { files, bytes };
321
+ }
322
+ async function runGit(remote, args) {
323
+ const credential = remoteCredential(remote.repository);
324
+ const askpassDirectory = credential ? await mkdtemp(join(tmpdir(), 'doxloop-askpass-')) : undefined;
325
+ try {
326
+ let askpass;
327
+ if (askpassDirectory) {
328
+ askpass = join(askpassDirectory, 'askpass.sh');
329
+ await writeFile(askpass, '#!/bin/sh\ncase "$1" in *sername*) printf "%s" "$DOXLOOP_GIT_USERNAME" ;; *) printf "%s" "$DOXLOOP_GIT_SECRET" ;; esac\n', { mode: 0o700 });
330
+ }
331
+ // An explicitly supplied UI credential must win over stale entries in the
332
+ // user's Git credential manager. Automatic mode still uses that manager.
333
+ const commandArgs = credential ? ['-c', 'credential.helper=', ...args] : args;
334
+ const result = await execFileAsync('git', commandArgs, {
335
+ maxBuffer: 20 * 1024 * 1024,
336
+ env: {
337
+ ...process.env,
338
+ GIT_TERMINAL_PROMPT: '0',
339
+ ...(askpass && credential ? {
340
+ GIT_ASKPASS: askpass,
341
+ DOXLOOP_GIT_USERNAME: credential.username ?? 'oauth2',
342
+ DOXLOOP_GIT_SECRET: credential.secret,
343
+ } : {}),
344
+ },
345
+ });
346
+ return result.stdout;
347
+ }
348
+ catch (error) {
349
+ const detail = error && typeof error === 'object' && 'stderr' in error ? String(error.stderr).trim() : '';
350
+ throw new DoxloopError(gitErrorMessage(detail, remote.repository));
351
+ }
352
+ finally {
353
+ if (askpassDirectory)
354
+ await rm(askpassDirectory, { recursive: true, force: true });
355
+ }
356
+ }
357
+ function remoteCredential(repository) {
358
+ const key = credentialKey(repository);
359
+ const remembered = gitCredentials.get(key);
360
+ if (remembered)
361
+ return remembered;
362
+ const serialized = process.env[GIT_CREDENTIALS_ENV];
363
+ if (!serialized)
364
+ return undefined;
365
+ try {
366
+ const value = JSON.parse(serialized);
367
+ if (!value || typeof value !== 'object' || Array.isArray(value))
368
+ return undefined;
369
+ const candidate = value[key];
370
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
371
+ return undefined;
372
+ const secret = candidate.secret;
373
+ const username = candidate.username;
374
+ if (typeof secret !== 'string' || !secret || (username !== undefined && typeof username !== 'string'))
375
+ return undefined;
376
+ return { ...(typeof username === 'string' && username ? { username } : {}), secret };
377
+ }
378
+ catch {
379
+ return undefined;
380
+ }
381
+ }
382
+ function gitErrorMessage(detail, repository) {
383
+ if (/write access to repository not granted|requested url returned error:\s*403|forbidden/i.test(detail)) {
384
+ const github = /github\.com/i.test(repository);
385
+ return github
386
+ ? 'GitHub denied access to this repository. Make sure the access key includes this repository and has Contents set to Read-only (or use a classic token with the repo scope). If the repository belongs to an organization, the key may also need SSO approval.'
387
+ : 'The Git service denied access to this repository. Make sure this account can read the repository and the access key has repository read permission.';
388
+ }
389
+ if (/authentication failed|could not read Username|terminal prompts disabled|access denied|repository not found/i.test(detail)) {
390
+ return 'Doxloop could not sign in to this repository. Check your repository address and access details.';
391
+ }
392
+ if (/not found|does not appear to be a git repository/i.test(detail))
393
+ return 'Doxloop could not find a Git repository at this address.';
394
+ return detail ? `Git connection failed: ${detail.split(/\r?\n/).slice(-2).join(' ')}` : 'Git connection failed. Check the repository address and try again.';
395
+ }
396
+ function credentialKey(repository) {
397
+ return repository.trim().replace(/\/$/, '');
398
+ }
399
+ async function githubRequest(remote, path, accept = 'application/vnd.github+json') {
400
+ const base = (remote.apiBaseUrl ?? 'https://api.github.com').replace(/\/$/, '');
401
+ const tokenName = remote.tokenEnv ?? 'GITHUB_TOKEN';
402
+ const token = process.env[tokenName];
403
+ const response = await fetch(`${base}${path}`, {
404
+ redirect: 'follow',
405
+ headers: {
406
+ Accept: accept,
407
+ 'User-Agent': 'doxloop-remote-source',
408
+ 'X-GitHub-Api-Version': '2022-11-28',
409
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
410
+ },
411
+ });
412
+ if (!response.ok) {
413
+ const hint = response.status === 401 || response.status === 403
414
+ ? ` Set ${tokenName} for a private repository or higher API limits.`
415
+ : '';
416
+ throw new DoxloopError(`GitHub API request failed for ${remote.repository} (${response.status} ${response.statusText}).${hint}`);
417
+ }
418
+ return response;
419
+ }
420
+ function repositoryPath(remote) {
421
+ return remote.repository.split('/').map(encodeURIComponent).join('/');
422
+ }
423
+ function safeName(value) {
424
+ return value.replace(/[^a-zA-Z0-9_-]/g, '-');
425
+ }
426
+ //# sourceMappingURL=remote-source.js.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Diff primitives for the local review center.
3
+ *
4
+ * Two kinds of diff are produced here. `unifiedRows` turns the stored proposal
5
+ * hunks back into a reviewable unified diff with surrounding context and
6
+ * word-level emphasis, and `alignBlocks`/`markHtmlWordDiff` line up the two
7
+ * rendered versions of a page so a reader can compare them side by side without
8
+ * reading Markdown at all.
9
+ */
10
+ import type { SyncChangeHunk } from './types.js';
11
+ export type SpanType = 'equal' | 'delete' | 'insert';
12
+ export interface DiffSpan {
13
+ type: SpanType;
14
+ beforeStart: number;
15
+ beforeEnd: number;
16
+ afterStart: number;
17
+ afterEnd: number;
18
+ }
19
+ /** Longest-common-subsequence diff over pre-computed comparison keys. */
20
+ export declare function diffKeys(before: readonly string[], after: readonly string[]): DiffSpan[];
21
+ export type DiffRowType = 'context' | 'delete' | 'insert' | 'gap';
22
+ export interface DiffRow {
23
+ type: DiffRowType;
24
+ /** 1-based line number in the current file, when the row exists there. */
25
+ oldNumber?: number;
26
+ /** 1-based line number in the proposed file, when the row exists there. */
27
+ newNumber?: number;
28
+ /** Escaped HTML — may contain `<mark>` spans for word-level emphasis. */
29
+ html: string;
30
+ /** Set on `gap` rows: how many unchanged lines were folded away. */
31
+ hidden?: number;
32
+ hunkId?: string;
33
+ hunkState?: 'pending' | 'accepted' | 'rejected';
34
+ }
35
+ export interface UnifiedDiff {
36
+ rows: DiffRow[];
37
+ added: number;
38
+ removed: number;
39
+ }
40
+ export interface UnifiedOptions {
41
+ context?: number;
42
+ }
43
+ /**
44
+ * Rebuild a reviewable unified diff from the proposal's stored hunks. The hunks
45
+ * only carry the changed lines, so the surrounding context comes from the
46
+ * snapshot of the file taken when the proposal was generated.
47
+ */
48
+ export declare function unifiedRows(beforeText: string, hunks: readonly SyncChangeHunk[], options?: UnifiedOptions): UnifiedDiff;
49
+ export interface WordDiff {
50
+ before: string;
51
+ after: string;
52
+ }
53
+ /** Word-level diff of two plain-text lines, returned as escaped HTML. */
54
+ export declare function wordDiffText(before: string, after: string): WordDiff;
55
+ /**
56
+ * Split rendered documentation HTML into its top-level blocks (one paragraph,
57
+ * heading, code block, callout, table … per entry) so two renderings can be
58
+ * lined up against each other.
59
+ */
60
+ export declare function splitHtmlBlocks(html: string): string[];
61
+ /** Visible text of an HTML fragment, used as the block comparison key. */
62
+ export declare function htmlText(html: string): string;
63
+ export type RenderedRowStatus = 'equal' | 'changed' | 'added' | 'removed';
64
+ export interface RenderedRow {
65
+ status: RenderedRowStatus;
66
+ before?: string;
67
+ after?: string;
68
+ }
69
+ /**
70
+ * Align the blocks of two rendered pages. Adjacent removals and additions are
71
+ * paired into a single "changed" row so the reader sees one before/after
72
+ * comparison instead of two disconnected entries.
73
+ */
74
+ export declare function alignBlocks(before: readonly string[], after: readonly string[]): RenderedRow[];
75
+ /**
76
+ * Highlight the words that differ between two rendered blocks while leaving the
77
+ * surrounding markup — links, code spans, table cells — intact.
78
+ */
79
+ export declare function markHtmlWordDiff(before: string, after: string): WordDiff;
80
+ export declare function textLines(text: string): string[];
81
+ export declare function escapeHtml(value: string): string;
82
+ //# sourceMappingURL=review-diff.d.ts.map