@doxbrix/doxloop 0.1.5 → 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 +1145 -0
  2. package/README.md +422 -227
  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 +24 -5
  22. package/dist/agents.js +60 -9
  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 +3 -0
  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 +25 -0
  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 +49 -32
  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,251 @@
1
+ import { readdir, readFile, stat } from 'node:fs/promises';
2
+ import { join, resolve, sep } from 'node:path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import { listFiles, pathExists } from './fs.js';
5
+ import { ROOT_CONTENT_IGNORED_DIRECTORIES } from './project.js';
6
+ /**
7
+ * Page extensions per generator when the adapter package cannot be loaded,
8
+ * so a folder can be inspected before anything is installed.
9
+ */
10
+ export const DEFAULT_PAGE_EXTENSIONS = {
11
+ doxbrix: ['.md', '.mdx'],
12
+ docusaurus: ['.md', '.mdx'],
13
+ mkdocs: ['.md'],
14
+ sphinx: ['.rst'],
15
+ hugo: ['.md'],
16
+ vitepress: ['.md'],
17
+ markdoc: ['.md'],
18
+ nextra: ['.md', '.mdx'],
19
+ starlight: ['.md', '.mdx'],
20
+ jekyll: ['.md'],
21
+ static: ['.html'],
22
+ };
23
+ /**
24
+ * Recognize an existing documentation site from the files its generator
25
+ * needs. Detection only reads; it never writes into the folder. Order matters
26
+ * where markers overlap: a Docusaurus or MkDocs site may also carry a Jekyll
27
+ * style `_config.yml`, so the more specific generator is listed first.
28
+ */
29
+ export async function detectDocumentationGenerator(root) {
30
+ const absolute = resolve(root);
31
+ const candidates = [];
32
+ for (const detector of DETECTORS) {
33
+ const candidate = await detector(absolute);
34
+ if (candidate && !candidates.some((item) => item.generator === candidate.generator))
35
+ candidates.push(candidate);
36
+ }
37
+ return { candidates, ...(candidates[0] ? { recommended: candidates[0] } : {}) };
38
+ }
39
+ /**
40
+ * Project-relative page paths under a content directory, using the
41
+ * generator's default extensions. Missing directories yield no pages instead
42
+ * of an error so an inspection can report "0 pages" for a wrong guess.
43
+ */
44
+ export async function listDocumentationPageFiles(root, generator, contentDir) {
45
+ const absoluteRoot = resolve(root);
46
+ const contentRoot = contentDir ? resolve(absoluteRoot, contentDir) : absoluteRoot;
47
+ try {
48
+ if (!(await stat(contentRoot)).isDirectory())
49
+ return [];
50
+ }
51
+ catch {
52
+ return [];
53
+ }
54
+ const files = await listFiles(contentRoot, new Set(DEFAULT_PAGE_EXTENSIONS[generator]), {
55
+ ignoredDirectories: contentRoot === absoluteRoot ? ROOT_CONTENT_IGNORED_DIRECTORIES : new Set(['node_modules', '.git']),
56
+ });
57
+ return files.map((file) => portable(file.slice(absoluteRoot.length + 1))).sort();
58
+ }
59
+ const DETECTORS = [
60
+ detectDoxbrix,
61
+ detectDocusaurus,
62
+ detectMkDocs,
63
+ detectStarlight,
64
+ detectVitePress,
65
+ detectNextra,
66
+ detectMarkdoc,
67
+ detectHugo,
68
+ detectSphinx,
69
+ detectJekyll,
70
+ ];
71
+ async function detectDoxbrix(root) {
72
+ for (const directory of ['', ...(await firstLevelDirectories(root))]) {
73
+ const marker = join(directory, 'docs.json');
74
+ const config = await readJsonObject(join(root, marker));
75
+ if (!config)
76
+ continue;
77
+ const spaces = Array.isArray(config.spaces);
78
+ const legacy = typeof config.title === 'string' && Array.isArray(config.navigation);
79
+ if (!spaces && !legacy)
80
+ continue;
81
+ const title = typeof config.name === 'string' ? config.name : typeof config.title === 'string' ? config.title : undefined;
82
+ return { generator: 'doxbrix', contentDir: portable(directory), markers: [portable(marker)], ...(title ? { title } : {}) };
83
+ }
84
+ return undefined;
85
+ }
86
+ async function detectDocusaurus(root) {
87
+ const marker = await firstExisting(root, ['docusaurus.config.js', 'docusaurus.config.ts', 'docusaurus.config.mjs', 'docusaurus.config.cjs']);
88
+ if (!marker)
89
+ return undefined;
90
+ const title = quotedValue(await readText(join(root, marker)), 'title');
91
+ return { generator: 'docusaurus', contentDir: 'docs', markers: [marker], ...(title ? { title } : {}) };
92
+ }
93
+ async function detectMkDocs(root) {
94
+ const marker = await firstExisting(root, ['mkdocs.yml', 'mkdocs.yaml']);
95
+ if (!marker)
96
+ return undefined;
97
+ const config = safeYaml(await readText(join(root, marker)));
98
+ const docsDir = typeof config?.docs_dir === 'string' && isSafeContentDir(config.docs_dir) ? config.docs_dir : 'docs';
99
+ const title = typeof config?.site_name === 'string' ? config.site_name : undefined;
100
+ return { generator: 'mkdocs', contentDir: portable(docsDir), markers: [marker], ...(title ? { title } : {}) };
101
+ }
102
+ async function detectStarlight(root) {
103
+ const marker = await firstExisting(root, ['astro.config.mjs', 'astro.config.js', 'astro.config.ts']);
104
+ if (!marker)
105
+ return undefined;
106
+ const content = await readText(join(root, marker));
107
+ if (!content.includes('starlight'))
108
+ return undefined;
109
+ const title = quotedValue(content, 'title');
110
+ return { generator: 'starlight', contentDir: 'src/content/docs', markers: [marker], ...(title ? { title } : {}) };
111
+ }
112
+ async function detectVitePress(root) {
113
+ const directories = ['docs', ...(await firstLevelDirectories(root)).filter((directory) => directory !== 'docs')];
114
+ for (const directory of directories) {
115
+ const marker = await firstExisting(root, ['config.mts', 'config.ts', 'config.js', 'config.mjs'].map((file) => join(directory, '.vitepress', file)));
116
+ if (!marker)
117
+ continue;
118
+ const title = quotedValue(await readText(join(root, marker)), 'title');
119
+ return { generator: 'vitepress', contentDir: portable(directory), markers: [portable(marker)], ...(title ? { title } : {}) };
120
+ }
121
+ return undefined;
122
+ }
123
+ async function detectNextra(root) {
124
+ const markers = [];
125
+ const nextConfig = await firstExisting(root, ['next.config.mjs', 'next.config.js', 'next.config.ts']);
126
+ if (nextConfig && (await readText(join(root, nextConfig))).includes('nextra'))
127
+ markers.push(nextConfig);
128
+ const theme = await firstExisting(root, ['theme.config.tsx', 'theme.config.jsx', 'theme.config.js']);
129
+ if (theme)
130
+ markers.push(theme);
131
+ if (markers.length === 0)
132
+ return undefined;
133
+ const contentDir = (await firstExisting(root, ['content', 'pages'])) ?? 'content';
134
+ return { generator: 'nextra', contentDir, markers };
135
+ }
136
+ async function detectMarkdoc(root) {
137
+ const marker = await firstExisting(root, ['markdoc.config.mjs', 'markdoc.config.js', 'markdoc.config.ts']);
138
+ if (!marker)
139
+ return undefined;
140
+ return { generator: 'markdoc', contentDir: 'docs', markers: [marker] };
141
+ }
142
+ async function detectHugo(root) {
143
+ const marker = await firstExisting(root, [
144
+ 'hugo.toml', 'hugo.yaml', 'hugo.yml', 'hugo.json',
145
+ 'config/_default/hugo.toml', 'config/_default/hugo.yaml', 'config/_default/hugo.yml',
146
+ 'config/_default/config.toml', 'config/_default/config.yaml', 'config/_default/config.yml',
147
+ ]);
148
+ let content = marker ? await readText(join(root, marker)) : '';
149
+ let found = marker;
150
+ if (!found) {
151
+ const generic = await firstExisting(root, ['config.toml', 'config.yaml', 'config.yml']);
152
+ if (!generic)
153
+ return undefined;
154
+ content = await readText(join(root, generic));
155
+ if (!/\b(baseURL|baseurl|languageCode|theme)\b/.test(content))
156
+ return undefined;
157
+ found = generic;
158
+ }
159
+ const contentDir = tomlOrYamlValue(content, 'contentDir') ?? 'content';
160
+ const title = tomlOrYamlValue(content, 'title');
161
+ return { generator: 'hugo', contentDir: isSafeContentDir(contentDir) ? portable(contentDir) : 'content', markers: [portable(found)], ...(title ? { title } : {}) };
162
+ }
163
+ async function detectSphinx(root) {
164
+ for (const directory of ['docs', 'doc', 'source', 'docs/source', 'doc/source']) {
165
+ const marker = join(directory, 'conf.py');
166
+ if (!(await pathExists(join(root, marker))))
167
+ continue;
168
+ const title = quotedValue(await readText(join(root, marker)), 'project');
169
+ return { generator: 'sphinx', contentDir: directory, markers: [portable(marker)], ...(title ? { title } : {}) };
170
+ }
171
+ return undefined;
172
+ }
173
+ async function detectJekyll(root) {
174
+ if (!(await pathExists(join(root, '_config.yml'))))
175
+ return undefined;
176
+ const config = safeYaml(await readText(join(root, '_config.yml')));
177
+ const contentDir = (await firstExisting(root, ['_docs', 'docs'])) ?? '_docs';
178
+ const title = typeof config?.title === 'string' ? config.title : undefined;
179
+ return { generator: 'jekyll', contentDir, markers: ['_config.yml'], ...(title ? { title } : {}) };
180
+ }
181
+ async function firstLevelDirectories(root) {
182
+ try {
183
+ const entries = await readdir(root, { withFileTypes: true });
184
+ return entries
185
+ .filter((entry) => entry.isDirectory() && !ROOT_CONTENT_IGNORED_DIRECTORIES.has(entry.name) && !entry.name.startsWith('.'))
186
+ .map((entry) => entry.name)
187
+ .sort();
188
+ }
189
+ catch {
190
+ return [];
191
+ }
192
+ }
193
+ async function firstExisting(root, paths) {
194
+ for (const path of paths)
195
+ if (await pathExists(join(root, path)))
196
+ return path;
197
+ return undefined;
198
+ }
199
+ async function readText(path) {
200
+ try {
201
+ return await readFile(path, 'utf8');
202
+ }
203
+ catch {
204
+ return '';
205
+ }
206
+ }
207
+ async function readJsonObject(path) {
208
+ const text = await readText(path);
209
+ if (!text.trim())
210
+ return undefined;
211
+ try {
212
+ const parsed = JSON.parse(text);
213
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : undefined;
214
+ }
215
+ catch {
216
+ return undefined;
217
+ }
218
+ }
219
+ function safeYaml(text) {
220
+ if (!text.trim())
221
+ return undefined;
222
+ try {
223
+ const parsed = parseYaml(text);
224
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : undefined;
225
+ }
226
+ catch {
227
+ // Configuration files with custom tags or template syntax still identify
228
+ // the generator; only their values become unavailable.
229
+ return undefined;
230
+ }
231
+ }
232
+ /** The first `key: 'value'` or `key = "value"` pair in a JavaScript, Python, or TOML file. */
233
+ function quotedValue(text, key) {
234
+ const match = new RegExp(`(?:^|[\\s{,])${key}\\s*[:=]\\s*(['"])([^'"\\n]+)\\1`, 'm').exec(text);
235
+ return match?.[2]?.trim() || undefined;
236
+ }
237
+ function tomlOrYamlValue(text, key) {
238
+ const quoted = quotedValue(text, key);
239
+ if (quoted)
240
+ return quoted;
241
+ const bare = new RegExp(`^\\s*${key}\\s*[:=]\\s*([^\\s#"']+)\\s*$`, 'mi').exec(text);
242
+ return bare?.[1]?.trim() || undefined;
243
+ }
244
+ function isSafeContentDir(value) {
245
+ const trimmed = value.trim();
246
+ return trimmed !== '' && !trimmed.startsWith('/') && !/^[a-zA-Z]:/.test(trimmed) && !trimmed.split(/[\\/]/).includes('..');
247
+ }
248
+ function portable(path) {
249
+ return sep === '/' ? path : path.split(sep).join('/');
250
+ }
251
+ //# sourceMappingURL=project-detect.js.map
@@ -0,0 +1,54 @@
1
+ import { type GeneratorDetection } from './project-detect.js';
2
+ import type { AgentName, GeneratorName } from './types.js';
3
+ export interface ExistingDocumentationInspection {
4
+ conversion?: 'mintlify';
5
+ root: string;
6
+ /** `.doxloop/project.json` already exists here; open it instead of importing. */
7
+ alreadyProject: boolean;
8
+ detection: GeneratorDetection;
9
+ generator?: GeneratorName;
10
+ contentDir?: string;
11
+ title: string;
12
+ markers: string[];
13
+ pageCount: number;
14
+ /** The first pages, project-relative, so the person can confirm the folder is the right one. */
15
+ pages: string[];
16
+ generatorInstalled: boolean;
17
+ generatorPackage?: string;
18
+ }
19
+ export interface ImportExistingDocumentationOptions {
20
+ directory: string;
21
+ generator?: GeneratorName;
22
+ contentDir?: string;
23
+ title?: string;
24
+ agent?: AgentName;
25
+ }
26
+ export interface ImportExistingDocumentationResult {
27
+ root: string;
28
+ title: string;
29
+ generator: GeneratorName;
30
+ contentDir: string;
31
+ pageCount: number;
32
+ skills: Array<{
33
+ path: string;
34
+ action: 'installed' | 'updated' | 'unchanged';
35
+ }>;
36
+ warnings: string[];
37
+ }
38
+ /**
39
+ * Read-only look at a folder: what generator it uses, where its pages are,
40
+ * and how many there are. Explicit choices override detection so the person
41
+ * can correct a wrong guess before anything is written.
42
+ */
43
+ export declare function inspectExistingDocumentation(directory: string, overrides?: {
44
+ generator?: GeneratorName;
45
+ contentDir?: string;
46
+ }): Promise<ExistingDocumentationInspection>;
47
+ /**
48
+ * Adopt an existing documentation folder as a Doxloop project. Import writes
49
+ * only Doxloop's own files (`.doxloop/project.json`, an evidence map that
50
+ * marks every page as unverified, `.gitignore` entries, and the agent
51
+ * skills) and runs a read-only discovery pass. It never modifies a page.
52
+ */
53
+ export declare function importExistingDocumentation(options: ImportExistingDocumentationOptions): Promise<ImportExistingDocumentationResult>;
54
+ //# sourceMappingURL=project-import.d.ts.map
@@ -0,0 +1,157 @@
1
+ import { mkdir, stat, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { installSkill } from './agents.js';
5
+ import { mintlifyMarker } from './mintlify-detect.js';
6
+ import { DoxloopError } from './errors.js';
7
+ import { EVIDENCE_MAP_FILE, readEvidenceMap, writeEvidenceMap } from './evidence.js';
8
+ import { ensureGitignoreEntries, pathExists, resolveContainedDirectory } from './fs.js';
9
+ import { generatorCatalogEntry, generatorPackageName, loadGeneratorAdapter, resolveGeneratorPackage } from './generators.js';
10
+ import { PROJECT_FILE, PROJECT_GITIGNORE_ENTRIES, defaultDocumentationBrief, defaultSyncConfig, loadPages, loadProject, relativePath, titleFromDirectory, } from './project.js';
11
+ import { detectDocumentationGenerator, listDocumentationPageFiles } from './project-detect.js';
12
+ import { discoverDocumentationSources } from './source-discovery.js';
13
+ const INSPECTION_PAGE_SAMPLE = 25;
14
+ /**
15
+ * Read-only look at a folder: what generator it uses, where its pages are,
16
+ * and how many there are. Explicit choices override detection so the person
17
+ * can correct a wrong guess before anything is written.
18
+ */
19
+ export async function inspectExistingDocumentation(directory, overrides = {}) {
20
+ const root = await resolveImportDirectory(directory);
21
+ const mintlify = await mintlifyMarker(root);
22
+ const alreadyProject = await pathExists(join(root, PROJECT_FILE));
23
+ const detection = await detectDocumentationGenerator(root);
24
+ const detected = overrides.generator
25
+ ? detection.candidates.find((candidate) => candidate.generator === overrides.generator)
26
+ : detection.recommended;
27
+ const generator = overrides.generator ?? detected?.generator;
28
+ const contentDir = overrides.contentDir !== undefined
29
+ ? normalizeContentDir(overrides.contentDir)
30
+ : detected?.contentDir ?? (generator ? defaultContentDir(generator) : undefined);
31
+ const pages = generator && contentDir !== undefined ? await listDocumentationPageFiles(root, generator, contentDir) : [];
32
+ const generatorPackage = generator ? generatorPackageName(generator) : undefined;
33
+ return {
34
+ ...(mintlify ? { conversion: 'mintlify' } : {}),
35
+ root,
36
+ alreadyProject,
37
+ detection,
38
+ ...(generator ? { generator } : {}),
39
+ ...(contentDir !== undefined ? { contentDir } : {}),
40
+ title: detected?.title ?? titleFromDirectory(root),
41
+ markers: detected?.markers ?? [],
42
+ pageCount: pages.length,
43
+ pages: pages.slice(0, INSPECTION_PAGE_SAMPLE),
44
+ generatorInstalled: !generatorPackage || resolveGeneratorPackage(root, generatorPackage) !== undefined,
45
+ ...(generatorPackage ? { generatorPackage } : {}),
46
+ };
47
+ }
48
+ /**
49
+ * Adopt an existing documentation folder as a Doxloop project. Import writes
50
+ * only Doxloop's own files (`.doxloop/project.json`, an evidence map that
51
+ * marks every page as unverified, `.gitignore` entries, and the agent
52
+ * skills) and runs a read-only discovery pass. It never modifies a page.
53
+ */
54
+ export async function importExistingDocumentation(options) {
55
+ const root = await resolveImportDirectory(options.directory);
56
+ if (await pathExists(join(root, PROJECT_FILE))) {
57
+ throw new DoxloopError(`${root} is already a Doxloop project. Open it instead of importing it.`, 2);
58
+ }
59
+ const inspection = await inspectExistingDocumentation(root, {
60
+ ...(options.generator ? { generator: options.generator } : {}),
61
+ ...(options.contentDir !== undefined ? { contentDir: options.contentDir } : {}),
62
+ });
63
+ if (inspection.conversion === 'mintlify')
64
+ throw new DoxloopError('Mintlify must be converted to a new Doxbrix project. Use Import existing documentation → Mintlify to Doxbrix in the control center.');
65
+ const generator = inspection.generator;
66
+ if (!generator) {
67
+ throw new DoxloopError(`Could not recognize the documentation generator in ${root}.\nChoose one explicitly with --generator, or start a new project instead.`, 2);
68
+ }
69
+ const contentDir = inspection.contentDir ?? defaultContentDir(generator);
70
+ if (contentDir === '' && generator !== 'doxbrix') {
71
+ throw new DoxloopError(`${generatorCatalogEntry(generator)?.displayName ?? generator} keeps its pages in a subdirectory. Choose the content directory explicitly.`, 2);
72
+ }
73
+ await resolveContainedDirectory(root, contentDir, 'Documentation content directory', { allowRoot: generator === 'doxbrix' });
74
+ const configuredPackage = generatorPackageName(generator);
75
+ if (configuredPackage && !resolveGeneratorPackage(root, configuredPackage)) {
76
+ throw new DoxloopError(`The ${generatorCatalogEntry(generator)?.displayName ?? generator} generator package is not installed.\nInstall it in the documentation folder with:\n doxloop generator add ${generator} --cwd ${root}`, 2);
77
+ }
78
+ const adapter = generator === 'doxbrix' ? undefined : await loadGeneratorAdapter(root, generator, configuredPackage);
79
+ const title = options.title?.trim() || inspection.title;
80
+ const project = {
81
+ schemaVersion: 1,
82
+ title,
83
+ contentDir,
84
+ generator,
85
+ ...(configuredPackage ? { generatorPackage: configuredPackage } : {}),
86
+ sources: [],
87
+ designReferences: [],
88
+ documentation: defaultDocumentationBrief(),
89
+ sync: defaultSyncConfig(),
90
+ };
91
+ await mkdir(join(root, '.doxloop'), { recursive: true });
92
+ await writeFile(join(root, PROJECT_FILE), `${JSON.stringify(project, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
93
+ const loaded = await loadProject(root);
94
+ const warnings = [];
95
+ const pages = (await loadPages(root, loaded)).map((path) => relativePath(root, path));
96
+ await seedEvidenceMap(root, pages);
97
+ await ensureGitignoreEntries(root, [...PROJECT_GITIGNORE_ENTRIES, ...(adapter?.project.gitignore ?? [])]);
98
+ const skills = await installSkill({ root, ...(options.agent ? { agent: options.agent } : {}) });
99
+ try {
100
+ await discoverDocumentationSources(root);
101
+ }
102
+ catch (error) {
103
+ warnings.push(`Discovery could not run yet: ${error instanceof Error ? error.message : String(error)}`);
104
+ }
105
+ return { root, title, generator, contentDir, pageCount: pages.length, skills, warnings };
106
+ }
107
+ /**
108
+ * Every existing page starts as unverified: no source produced it, so the
109
+ * first update run has to attach evidence before drift detection can trust
110
+ * it. An evidence map that is already present is kept as it is.
111
+ */
112
+ async function seedEvidenceMap(root, pages) {
113
+ if (await pathExists(join(root, EVIDENCE_MAP_FILE))) {
114
+ await readEvidenceMap(root);
115
+ return;
116
+ }
117
+ const map = { schemaVersion: 1, pages: {} };
118
+ for (const page of pages)
119
+ map.pages[page] = { sources: [], confidence: 'needs-human' };
120
+ await writeEvidenceMap(root, map);
121
+ }
122
+ async function resolveImportDirectory(directory) {
123
+ const root = resolve(directory);
124
+ let stats;
125
+ try {
126
+ stats = await stat(root);
127
+ }
128
+ catch {
129
+ throw new DoxloopError(`The documentation folder does not exist: ${root}`, 2);
130
+ }
131
+ if (!stats.isDirectory())
132
+ throw new DoxloopError(`The documentation folder is not a directory: ${root}`, 2);
133
+ if (root === resolve(homedir()) || dirname(root) === root) {
134
+ throw new DoxloopError('Choose the folder that holds the documentation site, not your home directory or the filesystem root.', 2);
135
+ }
136
+ return root;
137
+ }
138
+ function normalizeContentDir(value) {
139
+ return value.trim().replace(/\\/g, '/').replace(/^\.\/+/, '').replace(/^\/+|\/+$/g, '');
140
+ }
141
+ function defaultContentDir(generator) {
142
+ const defaults = {
143
+ doxbrix: '',
144
+ docusaurus: 'docs',
145
+ mkdocs: 'docs',
146
+ sphinx: 'docs',
147
+ hugo: 'content',
148
+ vitepress: 'docs',
149
+ markdoc: 'docs',
150
+ nextra: 'content',
151
+ starlight: 'src/content/docs',
152
+ jekyll: '_docs',
153
+ static: 'site',
154
+ };
155
+ return defaults[generator];
156
+ }
157
+ //# sourceMappingURL=project-import.js.map
@@ -0,0 +1,6 @@
1
+ import { DoxloopError } from './errors.js';
2
+ export declare class ProjectBusyError extends DoxloopError {
3
+ }
4
+ /** Reentrant within one operation; exclusive across UI, CLI and scheduled processes. */
5
+ export declare function withProjectLock<T>(root: string, name: string, work: () => Promise<T>, timeoutMs?: number): Promise<T>;
6
+ //# sourceMappingURL=project-lock.d.ts.map
@@ -0,0 +1,96 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { mkdir, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { setTimeout as delay } from 'node:timers/promises';
6
+ import { DoxloopError } from './errors.js';
7
+ const heldLocks = new AsyncLocalStorage();
8
+ export class ProjectBusyError extends DoxloopError {
9
+ }
10
+ /** Reentrant within one operation; exclusive across UI, CLI and scheduled processes. */
11
+ export async function withProjectLock(root, name, work, timeoutMs = 10_000) {
12
+ if (!/^[a-z-]+$/.test(name))
13
+ throw new Error('Invalid project lock name');
14
+ const directory = join(await realpath(root), '.doxloop', 'locks');
15
+ const path = join(directory, `${name}.lock`);
16
+ if (heldLocks.getStore()?.has(path))
17
+ return work();
18
+ await mkdir(directory, { recursive: true, mode: 0o700 });
19
+ const token = randomUUID();
20
+ const deadline = Date.now() + timeoutMs;
21
+ while (true) {
22
+ try {
23
+ await mkdir(path, { mode: 0o700 });
24
+ try {
25
+ await writeFile(join(path, 'owner.json'), JSON.stringify({ pid: process.pid, token }), { mode: 0o600 });
26
+ }
27
+ catch (error) {
28
+ await rm(path, { recursive: true, force: true });
29
+ throw error;
30
+ }
31
+ break;
32
+ }
33
+ catch (error) {
34
+ if (error.code !== 'EEXIST')
35
+ throw error;
36
+ let reaped = false;
37
+ if (await abandoned(path)) {
38
+ const claim = join(path, 'reaping');
39
+ let claimed = false;
40
+ try {
41
+ await mkdir(claim);
42
+ claimed = true;
43
+ // Another owner may have acquired the path since our initial observation.
44
+ if (await abandoned(path)) {
45
+ const stale = `${path}.${token}.abandoned`;
46
+ await rename(path, stale);
47
+ await rm(stale, { recursive: true, force: true });
48
+ claimed = false;
49
+ reaped = true;
50
+ }
51
+ }
52
+ catch (cause) {
53
+ if (!['ENOENT', 'EEXIST'].includes(cause.code ?? ''))
54
+ throw cause;
55
+ }
56
+ finally {
57
+ if (claimed)
58
+ await rm(claim, { recursive: true, force: true });
59
+ }
60
+ }
61
+ // A lock left by a dead process was just removed: take it now, even with no wait allowed.
62
+ if (reaped)
63
+ continue;
64
+ if (Date.now() >= deadline)
65
+ throw new ProjectBusyError(`Another ${name} operation is running for this project. Wait for it to finish and retry.`);
66
+ await delay(40);
67
+ }
68
+ }
69
+ try {
70
+ return await heldLocks.run(new Set([...(heldLocks.getStore() ?? []), path]), work);
71
+ }
72
+ finally {
73
+ const owner = await readFile(join(path, 'owner.json'), 'utf8').catch(() => '');
74
+ if (owner && JSON.parse(owner).token === token)
75
+ await rm(path, { recursive: true, force: true });
76
+ }
77
+ }
78
+ async function abandoned(path) {
79
+ try {
80
+ const owner = JSON.parse(await readFile(join(path, 'owner.json'), 'utf8'));
81
+ if (!Number.isInteger(owner.pid) || owner.pid <= 0)
82
+ return false;
83
+ try {
84
+ process.kill(owner.pid, 0);
85
+ return false;
86
+ }
87
+ catch (error) {
88
+ return error.code === 'ESRCH';
89
+ }
90
+ }
91
+ catch {
92
+ // A creator may not yet have written its owner record. Never steal a new lock.
93
+ return Date.now() - (await stat(path).catch(() => ({ mtimeMs: Date.now() }))).mtimeMs > 60_000;
94
+ }
95
+ }
96
+ //# sourceMappingURL=project-lock.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The user-level list of documentation projects the control center has
3
+ * opened. It lives outside every project so switching between two workspaces
4
+ * is one click, and it records only what is needed to show and reopen them.
5
+ */
6
+ export interface RecentProject {
7
+ path: string;
8
+ title: string;
9
+ generator: string;
10
+ lastOpenedAt: string;
11
+ /** The project file is gone. The entry stays until forgotten so the person can see where it was. */
12
+ missing?: boolean;
13
+ }
14
+ export declare const MAX_RECENT_PROJECTS = 20;
15
+ /** `DOXLOOP_HOME` overrides the location so tests and CI never touch a real home directory. */
16
+ export declare function doxloopHomeDirectory(): string;
17
+ export declare function projectRegistryPath(): string;
18
+ export declare function listRecentProjects(): Promise<RecentProject[]>;
19
+ export declare function rememberProject(entry: {
20
+ path: string;
21
+ title: string;
22
+ generator: string;
23
+ }): Promise<RecentProject[]>;
24
+ export declare function forgetProject(path: string): Promise<RecentProject[]>;
25
+ //# sourceMappingURL=project-registry.d.ts.map
@@ -0,0 +1,79 @@
1
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join, resolve } from 'node:path';
4
+ import { pathExists } from './fs.js';
5
+ import { PROJECT_FILE } from './project.js';
6
+ export const MAX_RECENT_PROJECTS = 20;
7
+ /** `DOXLOOP_HOME` overrides the location so tests and CI never touch a real home directory. */
8
+ export function doxloopHomeDirectory() {
9
+ const override = process.env.DOXLOOP_HOME?.trim();
10
+ return override ? resolve(override) : join(homedir(), '.doxloop');
11
+ }
12
+ export function projectRegistryPath() {
13
+ return join(doxloopHomeDirectory(), 'projects.json');
14
+ }
15
+ export async function listRecentProjects() {
16
+ const entries = await readRegistry();
17
+ return Promise.all(entries.map(async (entry) => ((await pathExists(join(entry.path, PROJECT_FILE))) ? entry : { ...entry, missing: true })));
18
+ }
19
+ export async function rememberProject(entry) {
20
+ const path = resolve(entry.path);
21
+ const others = (await readRegistry()).filter((item) => item.path !== path);
22
+ const next = [
23
+ { path, title: entry.title, generator: entry.generator, lastOpenedAt: new Date().toISOString() },
24
+ ...others,
25
+ ].slice(0, MAX_RECENT_PROJECTS);
26
+ await writeRegistry(next);
27
+ return next;
28
+ }
29
+ export async function forgetProject(path) {
30
+ const target = resolve(path);
31
+ const next = (await readRegistry()).filter((item) => item.path !== target);
32
+ await writeRegistry(next);
33
+ return next;
34
+ }
35
+ async function readRegistry() {
36
+ let raw;
37
+ try {
38
+ raw = await readFile(projectRegistryPath(), 'utf8');
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ let parsed;
44
+ try {
45
+ parsed = JSON.parse(raw);
46
+ }
47
+ catch {
48
+ // The registry is a convenience. A damaged file must never keep the
49
+ // control center from opening; it is rewritten on the next open.
50
+ return [];
51
+ }
52
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
53
+ return [];
54
+ const record = parsed;
55
+ if (record.schemaVersion !== 1 || !Array.isArray(record.projects))
56
+ return [];
57
+ return record.projects
58
+ .filter(isRecentProject)
59
+ .map((entry) => ({ path: resolve(entry.path), title: entry.title, generator: entry.generator, lastOpenedAt: entry.lastOpenedAt }))
60
+ .sort((left, right) => right.lastOpenedAt.localeCompare(left.lastOpenedAt));
61
+ }
62
+ async function writeRegistry(projects) {
63
+ const path = projectRegistryPath();
64
+ await mkdir(doxloopHomeDirectory(), { recursive: true, mode: 0o700 });
65
+ const temporary = `${path}.${process.pid}.tmp`;
66
+ const payload = projects.map(({ missing: _missing, ...entry }) => entry);
67
+ await writeFile(temporary, `${JSON.stringify({ schemaVersion: 1, projects: payload }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
68
+ await rename(temporary, path);
69
+ }
70
+ function isRecentProject(value) {
71
+ if (!value || typeof value !== 'object' || Array.isArray(value))
72
+ return false;
73
+ const entry = value;
74
+ return (typeof entry.path === 'string' && entry.path.trim() !== '' &&
75
+ typeof entry.title === 'string' &&
76
+ typeof entry.generator === 'string' && entry.generator.trim() !== '' &&
77
+ typeof entry.lastOpenedAt === 'string' && !Number.isNaN(Date.parse(entry.lastOpenedAt)));
78
+ }
79
+ //# sourceMappingURL=project-registry.js.map