@doxbrix/doxloop 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1153 -0
- package/README.md +422 -226
- package/assets/doxbrix-preview.css +77 -15
- package/contracts/agent-events-v1.schema.json +1 -0
- package/contracts/coverage-v1.schema.json +59 -0
- package/contracts/drift-v1.schema.json +1 -0
- package/contracts/evaluation-v1.schema.json +62 -0
- package/contracts/quality-codes-v1.json +1 -0
- package/contracts/quality-report-v1.schema.json +1 -0
- package/contracts/validation-v1.schema.json +1 -0
- package/dist/agent-failure.d.ts +36 -0
- package/dist/agent-failure.js +98 -0
- package/dist/agent-isolation.d.ts +17 -0
- package/dist/agent-isolation.js +94 -0
- package/dist/agent-log.d.ts +197 -0
- package/dist/agent-log.js +1094 -0
- package/dist/agent-process.d.ts +63 -0
- package/dist/agent-process.js +128 -0
- package/dist/agent-reply.d.ts +47 -0
- package/dist/agent-reply.js +315 -0
- package/dist/agents.d.ts +40 -4
- package/dist/agents.js +115 -11
- package/dist/api-coverage.d.ts +11 -0
- package/dist/api-coverage.js +72 -0
- package/dist/application-probe.d.ts +28 -0
- package/dist/application-probe.js +93 -0
- package/dist/application-url.d.ts +14 -0
- package/dist/application-url.js +26 -0
- package/dist/args.js +15 -0
- package/dist/artifact-deploy.d.ts +9 -0
- package/dist/artifact-deploy.js +28 -8
- package/dist/assets.d.ts +72 -0
- package/dist/assets.js +382 -0
- package/dist/auth.d.ts +10 -0
- package/dist/auth.js +38 -9
- package/dist/author.d.ts +206 -4
- package/dist/author.js +1650 -52
- package/dist/authoring-batches.d.ts +190 -0
- package/dist/authoring-batches.js +489 -0
- package/dist/authoring-postpass.d.ts +79 -0
- package/dist/authoring-postpass.js +1289 -0
- package/dist/authoring-progress.d.ts +95 -0
- package/dist/authoring-progress.js +269 -0
- package/dist/autosync.d.ts +56 -0
- package/dist/autosync.js +442 -0
- package/dist/batch-artifacts.d.ts +81 -0
- package/dist/batch-artifacts.js +412 -0
- package/dist/batch-limits.d.ts +37 -0
- package/dist/batch-limits.js +80 -0
- package/dist/branding.d.ts +41 -0
- package/dist/branding.js +229 -0
- package/dist/bundle-upload.d.ts +26 -0
- package/dist/bundle-upload.js +99 -0
- package/dist/capture-auth.d.ts +95 -0
- package/dist/capture-auth.js +204 -0
- package/dist/capture.d.ts +48 -0
- package/dist/capture.js +20 -1
- package/dist/cli.js +839 -32
- package/dist/content-links.d.ts +10 -0
- package/dist/content-links.js +49 -0
- package/dist/contract-validation.d.ts +4 -0
- package/dist/contract-validation.js +25 -0
- package/dist/coverage-actions.d.ts +8 -0
- package/dist/coverage-actions.js +71 -0
- package/dist/coverage-resolutions.d.ts +16 -0
- package/dist/coverage-resolutions.js +58 -0
- package/dist/db.d.ts +37 -0
- package/dist/db.js +288 -0
- package/dist/demo.d.ts +12 -0
- package/dist/demo.js +122 -0
- package/dist/deploy-credentials.d.ts +7 -0
- package/dist/deploy-credentials.js +76 -0
- package/dist/deploy-targets/github-pages.d.ts +3 -0
- package/dist/deploy-targets/github-pages.js +100 -0
- package/dist/deploy-targets/index.d.ts +8 -0
- package/dist/deploy-targets/index.js +78 -0
- package/dist/deploy-targets/netlify.d.ts +3 -0
- package/dist/deploy-targets/netlify.js +49 -0
- package/dist/deploy-targets/types.d.ts +32 -0
- package/dist/deploy-targets/types.js +2 -0
- package/dist/deploy-targets/vercel.d.ts +3 -0
- package/dist/deploy-targets/vercel.js +95 -0
- package/dist/deploy.d.ts +9 -0
- package/dist/deploy.js +87 -13
- package/dist/deterministic-capture.d.ts +90 -0
- package/dist/deterministic-capture.js +435 -0
- package/dist/direct-edit.d.ts +40 -0
- package/dist/direct-edit.js +159 -0
- package/dist/docs-crawl.d.ts +86 -0
- package/dist/docs-crawl.js +536 -0
- package/dist/docs-site.d.ts +49 -0
- package/dist/docs-site.js +233 -0
- package/dist/doctor.js +8 -0
- package/dist/documentation-collections.d.ts +23 -0
- package/dist/documentation-collections.js +191 -0
- package/dist/documentation-plan.d.ts +253 -0
- package/dist/documentation-plan.js +2563 -0
- package/dist/doxbrix-build.d.ts +19 -0
- package/dist/doxbrix-build.js +167 -0
- package/dist/doxbrix-markdown.d.ts +9 -0
- package/dist/doxbrix-markdown.js +115 -14
- package/dist/drift.d.ts +10 -0
- package/dist/drift.js +164 -0
- package/dist/evaluation.d.ts +46 -0
- package/dist/evaluation.js +113 -0
- package/dist/evidence-pack.d.ts +47 -0
- package/dist/evidence-pack.js +358 -0
- package/dist/evidence.d.ts +25 -0
- package/dist/evidence.js +175 -0
- package/dist/fs.d.ts +8 -2
- package/dist/fs.js +41 -11
- package/dist/generator-api.d.ts +89 -0
- package/dist/generator-preflight.d.ts +27 -0
- package/dist/generator-preflight.js +105 -0
- package/dist/generator-runtime.d.ts +7 -0
- package/dist/generator-runtime.js +17 -1
- package/dist/generators.d.ts +24 -2
- package/dist/generators.js +54 -1
- package/dist/git-delivery.d.ts +17 -0
- package/dist/git-delivery.js +123 -0
- package/dist/globs.d.ts +16 -0
- package/dist/globs.js +65 -0
- package/dist/glossary.d.ts +26 -0
- package/dist/glossary.js +179 -0
- package/dist/history.d.ts +106 -0
- package/dist/history.js +600 -0
- package/dist/html-markdown.d.ts +46 -0
- package/dist/html-markdown.js +423 -0
- package/dist/interactive.js +16 -15
- package/dist/job-events.d.ts +74 -0
- package/dist/job-events.js +377 -0
- package/dist/keep-awake.d.ts +50 -0
- package/dist/keep-awake.js +123 -0
- package/dist/local-source-snapshot.d.ts +20 -0
- package/dist/local-source-snapshot.js +61 -0
- package/dist/mintlify-detect.d.ts +3 -0
- package/dist/mintlify-detect.js +18 -0
- package/dist/mintlify-import.d.ts +75 -0
- package/dist/mintlify-import.js +190 -0
- package/dist/navigation.d.ts +98 -0
- package/dist/navigation.js +310 -0
- package/dist/openapi.d.ts +60 -0
- package/dist/openapi.js +439 -0
- package/dist/page-editor-bridge.d.ts +3 -0
- package/dist/page-editor-bridge.js +109 -0
- package/dist/page-editor-preview.d.ts +10 -0
- package/dist/page-editor-preview.js +55 -0
- package/dist/page-extension.d.ts +9 -0
- package/dist/page-extension.js +15 -0
- package/dist/page-metadata.d.ts +28 -0
- package/dist/page-metadata.js +166 -0
- package/dist/page-operations.d.ts +34 -0
- package/dist/page-operations.js +215 -0
- package/dist/page-routes.d.ts +4 -0
- package/dist/page-routes.js +61 -0
- package/dist/pages.d.ts +20 -0
- package/dist/pages.js +184 -0
- package/dist/plan-generator.d.ts +3 -0
- package/dist/plan-generator.js +21 -0
- package/dist/plan-navigation.d.ts +11 -0
- package/dist/plan-navigation.js +30 -0
- package/dist/planning-captures.d.ts +20 -0
- package/dist/planning-captures.js +143 -0
- package/dist/planning-research.d.ts +135 -0
- package/dist/planning-research.js +472 -0
- package/dist/planning-triage.d.ts +23 -0
- package/dist/planning-triage.js +131 -0
- package/dist/preview.d.ts +24 -0
- package/dist/preview.js +280 -29
- package/dist/project-detect.d.ts +36 -0
- package/dist/project-detect.js +251 -0
- package/dist/project-import.d.ts +54 -0
- package/dist/project-import.js +157 -0
- package/dist/project-lock.d.ts +6 -0
- package/dist/project-lock.js +96 -0
- package/dist/project-registry.d.ts +25 -0
- package/dist/project-registry.js +79 -0
- package/dist/project.d.ts +35 -3
- package/dist/project.js +388 -38
- package/dist/prompts.d.ts +9 -0
- package/dist/prompts.js +33 -4
- package/dist/proposal-replay.d.ts +32 -0
- package/dist/proposal-replay.js +99 -0
- package/dist/quality-claims.d.ts +8 -0
- package/dist/quality-claims.js +168 -0
- package/dist/quality-config.d.ts +5 -0
- package/dist/quality-config.js +84 -0
- package/dist/quality-contract.d.ts +37 -0
- package/dist/quality-contract.js +46 -0
- package/dist/quality-examples.d.ts +4 -0
- package/dist/quality-examples.js +233 -0
- package/dist/quality-gates.d.ts +16 -0
- package/dist/quality-gates.js +192 -0
- package/dist/quality-links.d.ts +7 -0
- package/dist/quality-links.js +149 -0
- package/dist/quality-lint.d.ts +6 -0
- package/dist/quality-lint.js +124 -0
- package/dist/quality-rendered.d.ts +11 -0
- package/dist/quality-rendered.js +222 -0
- package/dist/quality-schema.d.ts +3 -0
- package/dist/quality-schema.js +71 -0
- package/dist/release-notes.d.ts +44 -0
- package/dist/release-notes.js +183 -0
- package/dist/remote-monitor.d.ts +16 -0
- package/dist/remote-monitor.js +74 -0
- package/dist/remote-source.d.ts +34 -0
- package/dist/remote-source.js +426 -0
- package/dist/review-diff.d.ts +82 -0
- package/dist/review-diff.js +400 -0
- package/dist/review-learning.d.ts +11 -0
- package/dist/review-learning.js +60 -0
- package/dist/review-render.d.ts +38 -0
- package/dist/review-render.js +224 -0
- package/dist/review-report.d.ts +9 -0
- package/dist/review-report.js +89 -0
- package/dist/review-ui.d.ts +14 -0
- package/dist/review-ui.js +1248 -0
- package/dist/schedule.d.ts +78 -0
- package/dist/schedule.js +480 -0
- package/dist/screen-capture-provider.d.ts +52 -0
- package/dist/screen-capture-provider.js +218 -0
- package/dist/screenshot-workflow.d.ts +167 -0
- package/dist/screenshot-workflow.js +1237 -0
- package/dist/settings.d.ts +1 -1
- package/dist/settings.js +95 -7
- package/dist/site-export.d.ts +18 -0
- package/dist/site-export.js +87 -0
- package/dist/source-connectors.d.ts +33 -0
- package/dist/source-connectors.js +268 -0
- package/dist/source-discovery.d.ts +132 -0
- package/dist/source-discovery.js +823 -0
- package/dist/source-intelligence.d.ts +9 -0
- package/dist/source-intelligence.js +306 -0
- package/dist/sync-review.d.ts +28 -0
- package/dist/sync-review.js +264 -0
- package/dist/sync-runs.d.ts +192 -0
- package/dist/sync-runs.js +2244 -0
- package/dist/sync.d.ts +35 -0
- package/dist/sync.js +298 -32
- package/dist/text-diff.d.ts +9 -0
- package/dist/text-diff.js +59 -0
- package/dist/types.d.ts +946 -1
- package/dist/ui/assets/doxloop-logo-light-De7Nx7j7.png +0 -0
- package/dist/ui/assets/index-BHBYU2aG.css +1 -0
- package/dist/ui/assets/index-Cq3RPQiC.js +33 -0
- package/dist/ui/index.html +18 -0
- package/dist/ui-server.d.ts +83 -0
- package/dist/ui-server.js +3532 -0
- package/dist/usage-budget.d.ts +28 -0
- package/dist/usage-budget.js +90 -0
- package/dist/validation.d.ts +25 -1
- package/dist/validation.js +312 -26
- package/dist/workspace-tools.d.ts +54 -0
- package/dist/workspace-tools.js +123 -0
- package/docs/agent-compatibility.md +50 -28
- package/docs/ci-and-automation.md +105 -66
- package/docs/doxbrix-http-api.md +8 -1
- package/docs/existing-documentation.md +80 -0
- package/docs/generation-performance.md +108 -0
- package/docs/generator-authoring.md +68 -5
- package/docs/generator-selection.md +50 -13
- package/docs/mintlify-import.md +71 -0
- package/docs/openapi-security.md +25 -0
- package/docs/project-format.md +331 -36
- package/docs/release-quality.md +158 -0
- package/docs/releasing.md +72 -0
- package/docs/review-workflows.md +51 -0
- package/docs/security-model.md +102 -39
- package/docs/troubleshooting.md +226 -93
- package/package.json +46 -15
- package/scripts/test-auto-screenshot.mjs +172 -0
- package/skills/doxloop-authoring/SKILL.md +242 -361
- package/skills/doxloop-authoring/references/existing-documentation.md +94 -0
- package/skills/doxloop-authoring/references/navigation-architecture.md +18 -7
- package/skills/doxloop-authoring/references/page-depth.md +169 -0
- package/skills/doxloop-authoring/references/project-format.md +106 -7
- package/skills/doxloop-authoring/references/quality.md +10 -0
- package/skills/doxloop-authoring/references/screenshot-manifest.md +113 -0
- package/skills/doxloop-authoring/references/screenshots.md +149 -235
- package/skills/doxloop-authoring/references/workflows.md +76 -0
- package/skills/doxloop-doxbrix/SKILL.md +38 -22
- package/skills/doxloop-doxbrix/references/api-endpoints.md +15 -14
- package/skills/doxloop-doxbrix/references/components.md +30 -3
- package/skills/doxloop-doxbrix/references/manifest.md +3 -2
- package/vendor/doxbrix-import/LICENSE +202 -0
- package/vendor/doxbrix-import/README.md +17 -0
- package/vendor/doxbrix-import/UPSTREAM.json +47 -0
- package/vendor/doxbrix-import/dist/docs/frontmatter.d.ts +13 -0
- package/vendor/doxbrix-import/dist/docs/frontmatter.js +83 -0
- package/vendor/doxbrix-import/dist/docs/import.d.ts +21 -0
- package/vendor/doxbrix-import/dist/docs/import.js +147 -0
- package/vendor/doxbrix-import/dist/docs/manifest.d.ts +163 -0
- package/vendor/doxbrix-import/dist/docs/manifest.js +64 -0
- package/vendor/doxbrix-import/dist/docs/project.d.ts +25 -0
- package/vendor/doxbrix-import/dist/docs/project.js +77 -0
- package/vendor/doxbrix-import/dist/docs/starter.d.ts +4 -0
- package/vendor/doxbrix-import/dist/docs/starter.js +11 -0
- package/vendor/doxbrix-import/dist/importer.d.ts +227 -0
- package/vendor/doxbrix-import/dist/importer.js +1567 -0
- package/vendor/doxbrix-import/dist/mintlify-openapi.d.ts +37 -0
- package/vendor/doxbrix-import/dist/mintlify-openapi.js +305 -0
- package/vendor/doxbrix-import/dist/safe-path.d.ts +9 -0
- package/vendor/doxbrix-import/dist/safe-path.js +47 -0
- package/dist/agents.d.ts.map +0 -1
- package/dist/agents.js.map +0 -1
- package/dist/args.d.ts.map +0 -1
- package/dist/args.js.map +0 -1
- package/dist/artifact-deploy.d.ts.map +0 -1
- package/dist/artifact-deploy.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/author.d.ts.map +0 -1
- package/dist/author.js.map +0 -1
- package/dist/capture.d.ts.map +0 -1
- package/dist/capture.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/deploy.d.ts.map +0 -1
- package/dist/deploy.js.map +0 -1
- package/dist/deployment-visibility.d.ts.map +0 -1
- package/dist/deployment-visibility.js.map +0 -1
- package/dist/doctor.d.ts.map +0 -1
- package/dist/doctor.js.map +0 -1
- package/dist/doxbrix-markdown.d.ts.map +0 -1
- package/dist/doxbrix-markdown.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/fs.d.ts.map +0 -1
- package/dist/fs.js.map +0 -1
- package/dist/generator-api.d.ts.map +0 -1
- package/dist/generator-api.js.map +0 -1
- package/dist/generator-manager.d.ts.map +0 -1
- package/dist/generator-manager.js.map +0 -1
- package/dist/generator-runtime.d.ts.map +0 -1
- package/dist/generator-runtime.js.map +0 -1
- package/dist/generators.d.ts.map +0 -1
- package/dist/generators.js.map +0 -1
- package/dist/interactive.d.ts.map +0 -1
- package/dist/interactive.js.map +0 -1
- package/dist/preview.d.ts.map +0 -1
- package/dist/preview.js.map +0 -1
- package/dist/progress.d.ts.map +0 -1
- package/dist/progress.js.map +0 -1
- package/dist/project.d.ts.map +0 -1
- package/dist/project.js.map +0 -1
- package/dist/prompts.d.ts.map +0 -1
- package/dist/prompts.js.map +0 -1
- package/dist/settings.d.ts.map +0 -1
- package/dist/settings.js.map +0 -1
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- 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
|
-
|
|
199
|
-
await
|
|
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 ?? '
|
|
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
|
-
|
|
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 =
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
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(
|
|
435
|
-
await writeFile(join(
|
|
436
|
-
await writeFile(join(
|
|
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;
|