@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
package/dist/project.js CHANGED
@@ -1,11 +1,21 @@
1
+ import { documentationCollections } from './documentation-collections.js';
1
2
  import { access, mkdir, readFile, readdir, realpath, stat, writeFile, } from 'node:fs/promises';
2
3
  import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep, } from 'node:path';
3
4
  import matter from 'gray-matter';
4
5
  import { DoxloopError } from './errors.js';
5
- import { listFiles, pathExists, readJson, resolveContainedDirectory, } from './fs.js';
6
+ import { listFiles, ensureGitignoreEntries, pathExists, readJson, resolveContainedDirectory, } from './fs.js';
6
7
  import { generatorCatalogEntry, generatorPackageName, loadGeneratorAdapter, } from './generators.js';
7
8
  import { PACKAGE_NAME, VERSION } from './version.js';
8
9
  export const PROJECT_FILE = join('.doxloop', 'project.json');
10
+ export const ROOT_CONTENT_IGNORED_DIRECTORIES = new Set([
11
+ '.agents',
12
+ '.claude',
13
+ '.codex',
14
+ '.doxloop',
15
+ '.gemini',
16
+ '.git',
17
+ 'node_modules',
18
+ ]);
9
19
  export function defaultDocumentationBrief() {
10
20
  return {
11
21
  locale: 'en-US',
@@ -15,8 +25,36 @@ export function defaultDocumentationBrief() {
15
25
  terminology: {},
16
26
  exclusions: [],
17
27
  accessibilityTarget: 'WCAG 2.2 AA',
28
+ preferredExamples: [],
18
29
  };
19
30
  }
31
+ export function completeDocumentationBriefForCreate(brief) {
32
+ const primaryAudience = brief.primaryAudience || brief.audiences?.join(', ') || 'Developers';
33
+ const priorityOutcomes = brief.priorityOutcomes?.length
34
+ ? brief.priorityOutcomes
35
+ : ['Reach a first successful result'];
36
+ return { ...brief, primaryAudience, priorityOutcomes };
37
+ }
38
+ /**
39
+ * Lock files and snapshots never change reader-visible behavior. Test files are
40
+ * deliberately absent: the authoring workflow treats tests as evidence of
41
+ * supported behavior, so a changed test can legitimately change documentation.
42
+ */
43
+ const DEFAULT_SYNC_IGNORE = [
44
+ 'Cargo.lock',
45
+ 'Gemfile.lock',
46
+ 'composer.lock',
47
+ 'go.sum',
48
+ 'package-lock.json',
49
+ 'pnpm-lock.yaml',
50
+ 'poetry.lock',
51
+ 'yarn.lock',
52
+ '**/__snapshots__/**',
53
+ '*.snap',
54
+ ];
55
+ export function defaultSyncConfig() {
56
+ return { mode: 'check', on: [], watch: [], ignore: [...DEFAULT_SYNC_IGNORE] };
57
+ }
20
58
  export async function findProjectRoot(start) {
21
59
  let current = resolve(start);
22
60
  while (true) {
@@ -36,7 +74,7 @@ export async function loadProject(root) {
36
74
  if (project.schemaVersion !== 1 ||
37
75
  typeof project.title !== 'string' ||
38
76
  typeof project.contentDir !== 'string' ||
39
- !isSafeRelativeDirectory(project.contentDir) ||
77
+ !isSafeRelativeDirectory(project.contentDir, selectedGenerator === 'doxbrix') ||
40
78
  !Array.isArray(project.sources) ||
41
79
  (project.designReferences !== undefined &&
42
80
  !isDesignReferences(project.designReferences)) ||
@@ -53,10 +91,14 @@ export async function loadProject(root) {
53
91
  !isSourceBindings(project.sources) ||
54
92
  (project.defaultAgent !== undefined &&
55
93
  !['codex', 'claude', 'gemini'].includes(project.defaultAgent)) ||
94
+ (project.defaultModel !== undefined &&
95
+ (typeof project.defaultModel !== 'string' || project.defaultModel.trim() === '')) ||
56
96
  (project.documentation !== undefined &&
57
- !isDocumentationBrief(project.documentation))) {
97
+ !isDocumentationBrief(project.documentation)) ||
98
+ (project.sync !== undefined && !isSyncConfig(project.sync))) {
58
99
  throw new DoxloopError(`${PROJECT_FILE} has an unsupported format.`);
59
100
  }
101
+ validateSourceScopes(project.sources);
60
102
  const generator = selectedGenerator;
61
103
  return {
62
104
  schemaVersion: 1,
@@ -67,13 +109,24 @@ export async function loadProject(root) {
67
109
  ? { generatorPackage: project.generatorPackage }
68
110
  : {}),
69
111
  ...(project.defaultAgent ? { defaultAgent: project.defaultAgent } : {}),
112
+ ...(project.defaultAgent && project.defaultModel ? { defaultModel: project.defaultModel } : {}),
70
113
  sources: project.sources,
71
114
  designReferences: project.designReferences ?? [],
72
115
  ...(project.application ? { application: project.application } : {}),
73
116
  ...(project.deployment ? { deployment: project.deployment } : {}),
74
117
  documentation: project.documentation ?? defaultDocumentationBrief(),
118
+ sync: { ...defaultSyncConfig(), ...(project.sync ?? {}) },
75
119
  };
76
120
  }
121
+ /**
122
+ * The project's saved default model, but only for the agent it was chosen
123
+ * for: a Codex model ID must not be handed to Claude Code or Gemini.
124
+ */
125
+ export function projectDefaultModel(project, agent) {
126
+ if (!agent || !project.defaultModel || agent !== project.defaultAgent)
127
+ return undefined;
128
+ return project.defaultModel;
129
+ }
77
130
  export async function saveDefaultAgent(root, agent) {
78
131
  const path = join(root, PROJECT_FILE);
79
132
  const raw = await readJson(path);
@@ -83,7 +136,7 @@ export async function saveProjectSettings(root, changes) {
83
136
  const path = join(root, PROJECT_FILE);
84
137
  const raw = await readJson(path);
85
138
  const next = { ...raw, ...changes };
86
- for (const key of ['defaultAgent', 'application', 'deployment']) {
139
+ for (const key of ['defaultAgent', 'defaultModel', 'application', 'deployment', 'sync']) {
87
140
  if (key in changes && changes[key] === undefined)
88
141
  delete next[key];
89
142
  }
@@ -97,7 +150,7 @@ export async function saveProjectSettings(root, changes) {
97
150
  }
98
151
  export async function siteConfigPath(root, project) {
99
152
  const loaded = project ?? (await loadProject(root));
100
- const contentRoot = await resolveContainedDirectory(root, loaded.contentDir, 'Documentation content directory');
153
+ const contentRoot = await resolveContainedDirectory(root, loaded.contentDir, 'Documentation content directory', { allowRoot: loaded.generator === 'doxbrix' });
101
154
  const insideContent = join(contentRoot, 'docs.json');
102
155
  if (await pathExists(insideContent))
103
156
  return insideContent;
@@ -176,6 +229,39 @@ function isSpecLocation(location) {
176
229
  export function sourceKind(source) {
177
230
  return source.kind ?? 'directory';
178
231
  }
232
+ /**
233
+ * `--docs name=url` or `--docs url`: an existing documentation website to crawl.
234
+ * The returned binding is not yet materialized; its `path` holds the URL until
235
+ * the crawl writes the snapshot and replaces it with the snapshot directory.
236
+ */
237
+ export function parseDocsSite(raw) {
238
+ const equals = raw.indexOf('=');
239
+ const looksNamed = equals > 0 && !/^https?:/i.test(raw.slice(0, equals));
240
+ const name = looksNamed ? raw.slice(0, equals).trim() : 'docs';
241
+ const location = (looksNamed ? raw.slice(equals + 1) : raw).trim();
242
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
243
+ throw new DoxloopError(`Invalid source name "${name}".`, 2);
244
+ }
245
+ if (!isSpecUrl(location)) {
246
+ throw new DoxloopError(`Invalid documentation site "${raw}". Use the HTTP(S) address of the existing documentation, for example --docs https://docs.example.com or --docs legacy=https://docs.example.com/guide/.`, 2);
247
+ }
248
+ let url;
249
+ try {
250
+ url = new URL(location);
251
+ }
252
+ catch {
253
+ throw new DoxloopError(`Invalid documentation site URL "${location}".`, 2);
254
+ }
255
+ if (url.username || url.password) {
256
+ throw new DoxloopError(`Invalid documentation site URL "${location}". Remove embedded credentials.`, 2);
257
+ }
258
+ url.hash = '';
259
+ return { name, path: url.toString(), kind: 'docs-site' };
260
+ }
261
+ /** A `docs-site` binding whose snapshot has not been crawled yet: its path is still the URL. */
262
+ export function isUnmaterializedDocsSite(source) {
263
+ return sourceKind(source) === 'docs-site' && isSpecUrl(source.path);
264
+ }
179
265
  export async function resolveSeparateProjectLayout(options) {
180
266
  const sourceRoot = resolve(options.cwd, options.source);
181
267
  const projectRoot = resolve(options.cwd, options.output);
@@ -192,11 +278,17 @@ export async function resolveSeparateProjectLayout(options) {
192
278
  };
193
279
  }
194
280
  export async function validateProjectSourceBoundaries(projectRoot, sources) {
281
+ validateSourceScopes(sources);
195
282
  for (const source of sources) {
196
283
  if (sourceKind(source) === 'openapi') {
284
+ await (await import('./openapi.js')).loadOpenApiSource(projectRoot, source);
285
+ continue;
286
+ }
287
+ if (sourceKind(source) === 'docs-site') {
197
288
  if (isSpecUrl(source.path))
198
- continue;
199
- await assertSpecFile(resolve(projectRoot, source.path));
289
+ throw new DoxloopError(`Documentation site source "${source.name}" has not been crawled yet: ${source.path}`);
290
+ await (await import('./docs-site.js')).assertDocsSiteSnapshot(projectRoot, source);
291
+ await assertSeparateDirectories(resolve(projectRoot, source.path), projectRoot);
200
292
  continue;
201
293
  }
202
294
  const sourceRoot = resolve(projectRoot, source.path);
@@ -204,6 +296,24 @@ export async function validateProjectSourceBoundaries(projectRoot, sources) {
204
296
  await assertSeparateDirectories(sourceRoot, projectRoot);
205
297
  }
206
298
  }
299
+ export function validateSourceScopes(sources) {
300
+ const prefixes = sources.flatMap((source) => source.scope?.routePrefix ? [{ source: source.name, prefix: normalizeScopePath(source.scope.routePrefix) }] : []);
301
+ for (let index = 0; index < prefixes.length; index += 1) {
302
+ for (let other = index + 1; other < prefixes.length; other += 1) {
303
+ const left = prefixes[index];
304
+ const right = prefixes[other];
305
+ if (left.prefix === right.prefix || left.prefix.startsWith(`${right.prefix}/`) || right.prefix.startsWith(`${left.prefix}/`)) {
306
+ throw new DoxloopError(`Source scopes for "${left.source}" and "${right.source}" overlap at "${left.prefix}" and "${right.prefix}". Give each source a distinct route prefix; use sharedPages for intentional shared pages.`);
307
+ }
308
+ }
309
+ }
310
+ }
311
+ function normalizeScopePath(value) {
312
+ const normalized = value.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
313
+ if (!normalized || normalized.split('/').includes('..'))
314
+ throw new DoxloopError('Source route prefixes must be safe relative paths.');
315
+ return normalized;
316
+ }
207
317
  export async function assertNewProjectDirectory(projectRoot) {
208
318
  if (!(await pathExists(projectRoot)))
209
319
  return;
@@ -301,7 +411,7 @@ export async function scaffoldProject(options) {
301
411
  const adapter = generator === 'doxbrix'
302
412
  ? undefined
303
413
  : await loadGeneratorAdapter(root, generator, configuredPackage);
304
- const contentDir = adapter?.project.defaultContentDir ?? 'docs';
414
+ const contentDir = adapter?.project.defaultContentDir ?? '';
305
415
  await mkdir(join(root, '.doxloop'), { recursive: true });
306
416
  await mkdir(join(root, contentDir), { recursive: true });
307
417
  const project = {
@@ -313,6 +423,7 @@ export async function scaffoldProject(options) {
313
423
  sources: options.sources,
314
424
  designReferences: options.designReferences ?? [],
315
425
  documentation: defaultDocumentationBrief(),
426
+ sync: defaultSyncConfig(),
316
427
  };
317
428
  await writeJson(join(root, PROJECT_FILE), project);
318
429
  if (adapter) {
@@ -328,24 +439,70 @@ export async function scaffoldProject(options) {
328
439
  });
329
440
  }
330
441
  else {
331
- await scaffoldDoxbrix(root, title);
442
+ await scaffoldDoxbrix(root, title, contentDir);
332
443
  }
333
444
  await mergeGitignore(root, [
334
- '.doxloop/cache/',
335
- '.doxloop/last-run.json',
445
+ ...PROJECT_GITIGNORE_ENTRIES,
336
446
  ...(adapter?.project.gitignore ?? []),
337
447
  ]);
338
448
  return root;
339
449
  }
450
+ /**
451
+ * Machine-local Doxloop state that never belongs in version control. Shared by
452
+ * the scaffold and by importing an existing documentation folder.
453
+ */
454
+ export const PROJECT_GITIGNORE_ENTRIES = [
455
+ '.doxloop/cache/',
456
+ '.doxloop/runs/',
457
+ '.doxloop/last-run.json',
458
+ '.doxloop/sync.log',
459
+ '.doxloop/sync.log.1',
460
+ '.doxloop/locks/',
461
+ '.doxloop/direct-edits/',
462
+ '.doxloop/monitor-budget.json',
463
+ '.doxloop/quality-reports/',
464
+ '.doxloop/quality-artifacts/',
465
+ '.doxloop/evaluations/',
466
+ '.doxloop/ui-jobs.json',
467
+ '.doxloop/ui-job-logs/',
468
+ '.doxloop/review-preferences.json',
469
+ '.doxloop/deliveries/',
470
+ '.doxloop/exports/',
471
+ '.doxloop/doxloop.db',
472
+ '.doxloop/doxloop.db-wal',
473
+ '.doxloop/doxloop.db-shm',
474
+ 'build/',
475
+ ];
340
476
  export async function loadPages(root, project) {
341
- const contentRoot = await resolveContainedDirectory(root, project.contentDir, 'Documentation content directory');
477
+ const contentRoot = await resolveContainedDirectory(root, project.contentDir, 'Documentation content directory', { allowRoot: project.generator === 'doxbrix' });
342
478
  await access(contentRoot);
343
- const extensions = project.generator === 'doxbrix'
344
- ? ['.md', '.mdx']
345
- : (await loadGeneratorAdapter(root, project)).project.pageExtensions;
346
- return listFiles(contentRoot, new Set(extensions));
479
+ const extensions = await pageExtensions(root, project);
480
+ const collections = await documentationCollections(root, project);
481
+ const sets = await Promise.all(collections.map(async (collection) => {
482
+ const directory = await resolveContainedDirectory(root, collection.directory, 'Collection directory', { allowRoot: project.generator === 'doxbrix' });
483
+ return listFiles(directory, extensions, { ...(directory === resolve(root) ? { ignoredDirectories: ROOT_CONTENT_IGNORED_DIRECTORIES } : {}) });
484
+ }));
485
+ return [...new Set(sets.flat())].sort();
486
+ }
487
+ /**
488
+ * File extensions the configured generator treats as readable pages. Everything
489
+ * else inside the content directory is site data, not documentation.
490
+ */
491
+ export async function pageExtensions(root, project) {
492
+ if (project.generator === 'doxbrix')
493
+ return new Set(['.md', '.mdx']);
494
+ try {
495
+ return new Set((await loadGeneratorAdapter(root, project)).project.pageExtensions);
496
+ }
497
+ catch {
498
+ // A generator package that cannot be loaded should not turn every data file
499
+ // into a reviewable page.
500
+ return new Set(['.md', '.mdx']);
501
+ }
347
502
  }
348
- function isSafeRelativeDirectory(value) {
503
+ function isSafeRelativeDirectory(value, allowRoot = false) {
504
+ if (value === '')
505
+ return allowRoot;
349
506
  if (value.trim() === '' || isAbsolute(value))
350
507
  return false;
351
508
  const normalized = normalize(value);
@@ -357,9 +514,15 @@ export async function readPage(path) {
357
514
  const parsed = matter(await readFile(path, 'utf8'));
358
515
  const title = typeof parsed.data.title === 'string' ? parsed.data.title.trim() : '';
359
516
  const description = typeof parsed.data.description === 'string' ? parsed.data.description.trim() : undefined;
360
- return description === undefined
361
- ? { title, body: parsed.content }
362
- : { title, description, body: parsed.content };
517
+ const canonical = typeof parsed.data.canonical === 'string' ? parsed.data.canonical.trim() : '';
518
+ const socialImage = typeof parsed.data.socialImage === 'string' ? parsed.data.socialImage.trim() : '';
519
+ return {
520
+ title,
521
+ ...(description === undefined ? {} : { description }),
522
+ body: parsed.content,
523
+ ...(canonical ? { canonical } : {}),
524
+ ...(socialImage ? { socialImage } : {}),
525
+ };
363
526
  }
364
527
  export function pageId(contentRoot, path) {
365
528
  return relativePath(contentRoot, path).replace(/\.[^.\/]+$/i, '');
@@ -371,26 +534,17 @@ async function writeJson(path, value) {
371
534
  await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
372
535
  }
373
536
  async function mergeGitignore(root, entries) {
374
- const path = join(root, '.gitignore');
375
- const existing = (await pathExists(path)) ? await readFile(path, 'utf8') : '';
376
- const lines = existing.split(/\r?\n/).filter(Boolean);
377
- const seen = new Set(lines);
378
- for (const entry of entries) {
379
- if (!seen.has(entry)) {
380
- lines.push(entry);
381
- seen.add(entry);
382
- }
383
- }
384
- await writeFile(path, `${lines.join('\n')}\n`, 'utf8');
537
+ await ensureGitignoreEntries(root, entries);
385
538
  }
386
- function titleFromDirectory(directory) {
539
+ export function titleFromDirectory(directory) {
387
540
  return basename(directory)
388
541
  .split(/[-_]+/)
389
542
  .filter(Boolean)
390
543
  .map((word) => `${word[0]?.toUpperCase() ?? ''}${word.slice(1)}`)
391
544
  .join(' ');
392
545
  }
393
- async function scaffoldDoxbrix(root, title) {
546
+ async function scaffoldDoxbrix(root, title, contentDir) {
547
+ const contentRoot = contentDir ? join(root, contentDir) : root;
394
548
  const site = {
395
549
  version: 1,
396
550
  name: title,
@@ -431,9 +585,9 @@ async function scaffoldDoxbrix(root, title) {
431
585
  codeFont: 'ui-monospace',
432
586
  },
433
587
  };
434
- await writeJson(join(root, 'docs', 'docs.json'), site);
435
- await writeFile(join(root, 'docs', 'index.mdx'), `---\ntitle: ${JSON.stringify(title)}\ndescription: "Understand what ${title} helps you accomplish and choose your first workflow."\nicon: compass\n---\n\n# ${title}\n\n<!-- doxloop:starter-page -->\n\n<Info>The authoring agent will replace this starter with an evidence-backed product overview.</Info>\n\n<CardGroup cols="2">\n<Card title="Quickstart" icon="🚀" href="/quickstart">Reach your first successful result.</Card>\n<Card title="Explore the product" icon="🧭">Discover the workflows supported by the configured source.</Card>\n</CardGroup>\n`, { encoding: 'utf8', flag: 'wx' });
436
- await writeFile(join(root, 'docs', 'quickstart.mdx'), `---\ntitle: "Quickstart"\ndescription: "Reach your first successful result with verified product instructions."\nicon: bolt\n---\n\n# Quickstart\n\n<!-- doxloop:starter-page -->\n\n<Steps>\n<Step title="Confirm the prerequisites">Use the authoring agent to identify supported requirements from source.</Step>\n<Step title="Complete the first workflow">Replace this starter with commands or code verified against the product.</Step>\n<Step title="Verify success">Show the observable result a reader should expect.</Step>\n</Steps>\n`, { encoding: 'utf8', flag: 'wx' });
588
+ await writeJson(join(contentRoot, 'docs.json'), site);
589
+ await writeFile(join(contentRoot, 'index.mdx'), `---\ntitle: ${JSON.stringify(title)}\ndescription: "Understand what ${title} helps you accomplish and choose your first workflow."\nicon: compass\n---\n\n# ${title}\n\n<!-- doxloop:starter-page -->\n\n<Info>The authoring agent will replace this starter with an evidence-backed product overview.</Info>\n\n<CardGroup cols="2">\n<Card title="Quickstart" icon="🚀" href="/quickstart">Reach your first successful result.</Card>\n<Card title="Explore the product" icon="🧭">Discover the workflows supported by the configured source.</Card>\n</CardGroup>\n`, { encoding: 'utf8', flag: 'wx' });
590
+ await writeFile(join(contentRoot, 'quickstart.mdx'), `---\ntitle: "Quickstart"\ndescription: "Reach your first successful result with verified product instructions."\nicon: bolt\n---\n\n# Quickstart\n\n<!-- doxloop:starter-page -->\n\n<Steps>\n<Step title="Confirm the prerequisites">Use the authoring agent to identify supported requirements from source.</Step>\n<Step title="Complete the first workflow">Replace this starter with commands or code verified against the product.</Step>\n<Step title="Verify success">Show the observable result a reader should expect.</Step>\n</Steps>\n`, { encoding: 'utf8', flag: 'wx' });
437
591
  }
438
592
  function isDoxbrixSiteConfig(value) {
439
593
  return doxbrixSiteConfigProblem(value) === undefined;
@@ -454,6 +608,29 @@ function doxbrixSiteConfigProblem(value) {
454
608
  }
455
609
  if (!Array.isArray(candidate.spaces))
456
610
  return 'spaces must be an array.';
611
+ if (candidate.versions !== undefined) {
612
+ if (!Array.isArray(candidate.versions))
613
+ return 'versions must be an array.';
614
+ const slugs = new Set();
615
+ for (const entry of candidate.versions) {
616
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || typeof entry.version !== 'string' || !entry.version.trim())
617
+ return 'Each documentation version needs a non-empty version string.';
618
+ if (slugs.has(entry.version))
619
+ return `Duplicate documentation version "${entry.version}".`;
620
+ slugs.add(entry.version);
621
+ for (const key of ['label', 'tag'])
622
+ if (entry[key] !== undefined && typeof entry[key] !== 'string')
623
+ return `Documentation version ${key} must be a string.`;
624
+ for (const key of ['default', 'isDefault'])
625
+ if (entry[key] !== undefined && typeof entry[key] !== 'boolean')
626
+ return `Documentation version ${key} must be a boolean.`;
627
+ }
628
+ if (candidate.versions.length)
629
+ for (const space of candidate.spaces) {
630
+ if (space && typeof space === 'object' && typeof space.version === 'string' && !slugs.has(space.version))
631
+ return `Space references unknown documentation version "${space.version}".`;
632
+ }
633
+ }
457
634
  for (const [spaceIndex, value] of candidate.spaces.entries()) {
458
635
  const location = `spaces[${spaceIndex}]`;
459
636
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -465,7 +642,7 @@ function doxbrixSiteConfigProblem(value) {
465
642
  }
466
643
  if (!Array.isArray(space.nav))
467
644
  return `${location}.nav must be an array.`;
468
- const optionalStrings = ['slug', 'locale', 'parent', 'icon', 'tag'];
645
+ const optionalStrings = ['slug', 'locale', 'parent', 'icon', 'tag', 'version'];
469
646
  for (const key of optionalStrings) {
470
647
  if (space[key] !== undefined && typeof space[key] !== 'string') {
471
648
  return `${location}.${key} must be a string.`;
@@ -571,7 +748,70 @@ function isSourceBindings(value) {
571
748
  typeof source.path === 'string' &&
572
749
  (source.kind === undefined ||
573
750
  source.kind === 'directory' ||
574
- source.kind === 'openapi')));
751
+ source.kind === 'openapi' ||
752
+ source.kind === 'docs-site') &&
753
+ isDocsSiteInfo(source.site) &&
754
+ isSourceScope(source.scope) &&
755
+ isRemoteSource(source.remote)));
756
+ }
757
+ function isDocsSiteInfo(value) {
758
+ if (value === undefined)
759
+ return true;
760
+ if (!value || typeof value !== 'object' || Array.isArray(value))
761
+ return false;
762
+ const site = value;
763
+ return typeof site.url === 'string' && typeof site.crawledAt === 'string' && typeof site.pages === 'number' && typeof site.hash === 'string';
764
+ }
765
+ function isSourceScope(value) {
766
+ if (value === undefined)
767
+ return true;
768
+ if (!value || typeof value !== 'object' || Array.isArray(value))
769
+ return false;
770
+ const scope = value;
771
+ const optionalText = (item) => item === undefined || (typeof item === 'string' && item.trim() !== '');
772
+ return optionalText(scope.space) && optionalText(scope.routePrefix) && optionalText(scope.navigationGroup) &&
773
+ (scope.sharedPages === undefined || (Array.isArray(scope.sharedPages) && scope.sharedPages.every((item) => typeof item === 'string' && item.trim() !== '')));
774
+ }
775
+ function isRemoteSource(value) {
776
+ if (value === undefined)
777
+ return true;
778
+ if (!value || typeof value !== 'object' || Array.isArray(value))
779
+ return false;
780
+ const remote = value;
781
+ if (remote.provider !== 'github' && remote.provider !== 'git')
782
+ return false;
783
+ if (typeof remote.repository !== 'string' ||
784
+ remote.repository.trim() === '' ||
785
+ typeof remote.branch !== 'string' ||
786
+ remote.branch.trim() === '')
787
+ return false;
788
+ if (remote.provider === 'github' && !/^[^/\s]+\/[^/\s]+$/.test(remote.repository))
789
+ return false;
790
+ if (remote.provider === 'git' &&
791
+ !/^(?:(?:https?|ssh|git|file):\/\/|[^@\s]+@[^:\s]+:)[^\r\n\0]+$/.test(remote.repository))
792
+ return false;
793
+ if (remote.tokenEnv !== undefined &&
794
+ (typeof remote.tokenEnv !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(remote.tokenEnv)))
795
+ return false;
796
+ if (remote.subdirectory !== undefined &&
797
+ (typeof remote.subdirectory !== 'string' ||
798
+ remote.subdirectory.trim() === '' ||
799
+ remote.subdirectory.startsWith('/') ||
800
+ remote.subdirectory.split(/[\\/]/).includes('..')))
801
+ return false;
802
+ if (remote.apiBaseUrl !== undefined) {
803
+ if (typeof remote.apiBaseUrl !== 'string')
804
+ return false;
805
+ try {
806
+ const url = new URL(remote.apiBaseUrl);
807
+ if (url.protocol !== 'https:' || url.username || url.password)
808
+ return false;
809
+ }
810
+ catch {
811
+ return false;
812
+ }
813
+ }
814
+ return true;
575
815
  }
576
816
  function isDesignReferences(value) {
577
817
  return (Array.isArray(value) &&
@@ -616,6 +856,14 @@ function isApplicationConfig(value, sources) {
616
856
  application.readyPath.startsWith('//'))) {
617
857
  return false;
618
858
  }
859
+ const authentication = application.authentication;
860
+ if (authentication !== undefined) {
861
+ if (!authentication || typeof authentication !== 'object' || Array.isArray(authentication))
862
+ return false;
863
+ if (authentication.loginPath !== undefined &&
864
+ (typeof authentication.loginPath !== 'string' || !validApplicationPath(authentication.loginPath)))
865
+ return false;
866
+ }
619
867
  const screenshots = application.screenshots;
620
868
  if (screenshots === undefined)
621
869
  return true;
@@ -627,6 +875,12 @@ function isApplicationConfig(value, sources) {
627
875
  if (screenshots.highlight !== undefined && typeof screenshots.highlight !== 'boolean') {
628
876
  return false;
629
877
  }
878
+ if (screenshots.startPath !== undefined &&
879
+ (typeof screenshots.startPath !== 'string' || !validApplicationPath(screenshots.startPath)))
880
+ return false;
881
+ if (screenshots.workflow !== undefined &&
882
+ (typeof screenshots.workflow !== 'string' || screenshots.workflow.trim().length < 12))
883
+ return false;
630
884
  if (screenshots.viewport === undefined)
631
885
  return true;
632
886
  return (Number.isInteger(screenshots.viewport.width) &&
@@ -636,6 +890,17 @@ function isApplicationConfig(value, sources) {
636
890
  screenshots.viewport.height >= 320 &&
637
891
  screenshots.viewport.height <= 2160);
638
892
  }
893
+ function validApplicationPath(value) {
894
+ if (!value.startsWith('/') || value.startsWith('//'))
895
+ return false;
896
+ try {
897
+ const parsed = new URL(value, 'https://application.invalid');
898
+ return parsed.origin === 'https://application.invalid' && !parsed.hash;
899
+ }
900
+ catch {
901
+ return false;
902
+ }
903
+ }
639
904
  function isDeploymentConfig(value) {
640
905
  if (!value || typeof value !== 'object' || Array.isArray(value))
641
906
  return false;
@@ -644,6 +909,12 @@ function isDeploymentConfig(value) {
644
909
  (typeof candidate === 'string' && candidate.trim() !== '');
645
910
  if (!optionalText(deployment.name) || !optionalText(deployment.slug))
646
911
  return false;
912
+ if (deployment.target !== undefined && !['doxbrix', 'github-pages', 'netlify', 'vercel'].includes(deployment.target))
913
+ return false;
914
+ if (![deployment.siteId, deployment.projectId, deployment.teamId, deployment.branch, deployment.basePath].every(optionalText))
915
+ return false;
916
+ if (deployment.basePath?.split('/').some((part) => part === '..'))
917
+ return false;
647
918
  if (deployment.visibility !== undefined &&
648
919
  !['private', 'public'].includes(deployment.visibility)) {
649
920
  return false;
@@ -677,6 +948,81 @@ function isApplicationBaseUrl(value) {
677
948
  return false;
678
949
  }
679
950
  }
951
+ export function parseSyncTrigger(raw) {
952
+ const value = raw.trim().toLowerCase();
953
+ const interval = /^every@(\d+)([mh])$/.exec(value);
954
+ if (interval) {
955
+ const amount = Number(interval[1]);
956
+ if ((interval[2] === 'm' && amount >= 1 && amount <= 59) ||
957
+ (interval[2] === 'h' && amount >= 1 && amount <= 24))
958
+ return value;
959
+ }
960
+ const daily = /^daily@(\d{2}):(\d{2})$/.exec(value);
961
+ if (daily) {
962
+ const hour = Number(daily[1]);
963
+ const minute = Number(daily[2]);
964
+ if (hour <= 23 && minute <= 59)
965
+ return value;
966
+ }
967
+ const weekdays = /^weekdays@(\d{2}):(\d{2})$/.exec(value);
968
+ if (weekdays && Number(weekdays[1]) <= 23 && Number(weekdays[2]) <= 59) {
969
+ return value;
970
+ }
971
+ const weekly = /^weekly@(sun|mon|tue|wed|thu|fri|sat)@(\d{2}):(\d{2})$/.exec(value);
972
+ if (weekly && Number(weekly[2]) <= 23 && Number(weekly[3]) <= 59) {
973
+ return value;
974
+ }
975
+ const monthly = /^monthly@(\d{1,2})@(\d{2}):(\d{2})$/.exec(value);
976
+ if (monthly && Number(monthly[1]) >= 1 && Number(monthly[1]) <= 28 && Number(monthly[2]) <= 23 && Number(monthly[3]) <= 59) {
977
+ return value;
978
+ }
979
+ throw new DoxloopError(`Invalid sync trigger "${raw}". Use daily, weekdays, weekly, monthly, or every interval syntax.`, 2);
980
+ }
981
+ function isSyncTrigger(value) {
982
+ if (typeof value !== 'string')
983
+ return false;
984
+ try {
985
+ parseSyncTrigger(value);
986
+ return true;
987
+ }
988
+ catch {
989
+ return false;
990
+ }
991
+ }
992
+ function isSyncConfig(value) {
993
+ if (!value || typeof value !== 'object' || Array.isArray(value))
994
+ return false;
995
+ const sync = value;
996
+ if (sync.mode !== undefined && !['check', 'propose', 'auto'].includes(sync.mode)) {
997
+ return false;
998
+ }
999
+ if (sync.branch !== undefined &&
1000
+ (typeof sync.branch !== 'string' || sync.branch.trim() === '')) {
1001
+ return false;
1002
+ }
1003
+ if (sync.on !== undefined && (!Array.isArray(sync.on) || !sync.on.every(isSyncTrigger))) {
1004
+ return false;
1005
+ }
1006
+ const patternList = (candidate) => candidate === undefined ||
1007
+ (Array.isArray(candidate) &&
1008
+ candidate.every((item) => typeof item === 'string' && item.trim() !== ''));
1009
+ if (!patternList(sync.watch) || !patternList(sync.ignore))
1010
+ return false;
1011
+ if (sync.maxVerificationAgeDays !== undefined && (!Number.isInteger(sync.maxVerificationAgeDays) || sync.maxVerificationAgeDays < 1 || sync.maxVerificationAgeDays > 3650))
1012
+ return false;
1013
+ if (sync.maxVerificationAgeSeverity !== undefined && !['warn', 'fail'].includes(sync.maxVerificationAgeSeverity))
1014
+ return false;
1015
+ if (sync.budget === undefined)
1016
+ return true;
1017
+ if (!sync.budget || typeof sync.budget !== 'object' || Array.isArray(sync.budget)) {
1018
+ return false;
1019
+ }
1020
+ const limit = (candidate, max) => candidate === undefined ||
1021
+ (Number.isInteger(candidate) && candidate >= 1 && candidate <= max);
1022
+ const amount = (candidate, max) => candidate === undefined ||
1023
+ (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0 && candidate <= max);
1024
+ return (limit(sync.budget.maxRunsPerDay, 1000) && limit(sync.budget.maxMinutes, 24 * 60) && amount(sync.budget.maxUsd, 100_000));
1025
+ }
680
1026
  function isDocumentationBrief(value) {
681
1027
  if (!value || typeof value !== 'object' || Array.isArray(value))
682
1028
  return false;
@@ -692,10 +1038,14 @@ function isDocumentationBrief(value) {
692
1038
  typeof preferred === 'string' &&
693
1039
  preferred.trim() !== '');
694
1040
  return (optionalText(brief.primaryAudience) &&
1041
+ (brief.audiences === undefined || textList(brief.audiences)) &&
1042
+ optionalText(brief.customInstructions) &&
695
1043
  (brief.experienceLevel === undefined ||
696
1044
  ['beginner', 'intermediate', 'advanced', 'mixed'].includes(brief.experienceLevel)) &&
697
1045
  (brief.priorityOutcomes === undefined ||
698
1046
  textList(brief.priorityOutcomes)) &&
1047
+ (brief.preferredExamples === undefined || textList(brief.preferredExamples)) &&
1048
+ optionalText(brief.designDirection) &&
699
1049
  typeof brief.locale === 'string' &&
700
1050
  brief.locale.trim() !== '' &&
701
1051
  textList(brief.tone) &&
package/dist/prompts.d.ts CHANGED
@@ -27,6 +27,15 @@ export declare function promptText(options: {
27
27
  validate?: (value: string) => string | undefined | Promise<string | undefined>;
28
28
  io?: PromptIo;
29
29
  }): Promise<string>;
30
+ /**
31
+ * Read one line without echoing it, for passwords. The readline interface
32
+ * writes its echo to a sink, so a terminal shows nothing while typing; piped
33
+ * input is read as a plain line.
34
+ */
35
+ export declare function promptSecret(options: {
36
+ message: string;
37
+ io?: PromptIo;
38
+ }): Promise<string>;
30
39
  export declare function promptConfirm(options: {
31
40
  message: string;
32
41
  initial?: boolean;