@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.
- package/CHANGELOG.md +1145 -0
- package/README.md +422 -227
- 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 +24 -5
- package/dist/agents.js +60 -9
- 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 +3 -0
- 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 +25 -0
- 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 +49 -32
- 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/demo.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { DoxloopError } from './errors.js';
|
|
5
|
+
import { approveDocumentationPlan, applyDocumentationPlanProposal, createDocumentationPlan } from './documentation-plan.js';
|
|
6
|
+
import { writeEvidenceMap } from './evidence.js';
|
|
7
|
+
import { scaffoldProject } from './project.js';
|
|
8
|
+
import { persistReviewReport } from './review-report.js';
|
|
9
|
+
import { recordSyncState } from './sync.js';
|
|
10
|
+
import { validateProject } from './validation.js';
|
|
11
|
+
/** Build a deterministic showcase in an isolated temporary directory. */
|
|
12
|
+
export async function createDemoWorkspace() {
|
|
13
|
+
const parent = await mkdtemp(join(tmpdir(), 'doxloop-demo-'));
|
|
14
|
+
const product = join(parent, 'product');
|
|
15
|
+
const root = join(parent, 'pulse-api-docs');
|
|
16
|
+
await mkdir(product, { recursive: true });
|
|
17
|
+
const specification = join(product, 'openapi.json');
|
|
18
|
+
await writeFile(specification, `${JSON.stringify(DEMO_OPENAPI, null, 2)}\n`, 'utf8');
|
|
19
|
+
await scaffoldProject({
|
|
20
|
+
directory: root,
|
|
21
|
+
title: 'Pulse API',
|
|
22
|
+
sources: [{ name: 'pulse-api', path: '../product/openapi.json', kind: 'openapi', scope: { routePrefix: 'reference', navigationGroup: 'API reference' } }],
|
|
23
|
+
});
|
|
24
|
+
for (const [path, content] of Object.entries(DEMO_PAGES)) {
|
|
25
|
+
await mkdir(join(root, path, '..'), { recursive: true });
|
|
26
|
+
await writeFile(join(root, path), content, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
await writeFile(join(root, 'docs.json'), `${JSON.stringify(DEMO_NAVIGATION, null, 2)}\n`, 'utf8');
|
|
29
|
+
const created = await createDocumentationPlan(root, {
|
|
30
|
+
mode: 'create',
|
|
31
|
+
scope: 'standard',
|
|
32
|
+
request: 'Create complete onboarding and API reference documentation for Pulse API.',
|
|
33
|
+
execution: { agent: 'codex', model: 'demo-bundled', reasoning: 'low', screenshots: false },
|
|
34
|
+
});
|
|
35
|
+
const pageDefinitions = Object.keys(DEMO_PAGES).map((path, index) => {
|
|
36
|
+
const id = path.replace(/\.mdx$/, '').replaceAll('/', '-');
|
|
37
|
+
const title = DEMO_TITLES[path];
|
|
38
|
+
return {
|
|
39
|
+
id,
|
|
40
|
+
title,
|
|
41
|
+
path: path.replace(/\.mdx$/, ''),
|
|
42
|
+
type: index < 2 ? 'getting-started' : path.startsWith('reference/') ? 'reference' : path.includes('troubleshooting') ? 'troubleshooting' : 'how-to',
|
|
43
|
+
priority: 'must-have',
|
|
44
|
+
action: index < 2 ? 'update' : 'create',
|
|
45
|
+
purpose: `Help developers ${DEMO_PURPOSES[path]}.`,
|
|
46
|
+
rationale: 'The bundled OpenAPI contract directly supports this reader task.',
|
|
47
|
+
evidence: [`pulse-api: ${DEMO_EVIDENCE[path]}`],
|
|
48
|
+
evidenceDetails: [{ source: 'pulse-api', path: 'openapi.json', kind: 'openapi', label: DEMO_EVIDENCE[path] }],
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
const ready = await applyDocumentationPlanProposal(root, created.id, {
|
|
52
|
+
productProfile: 'Event ingestion and webhook delivery API',
|
|
53
|
+
summary: 'Take a developer from authentication through event ingestion, webhook handling, and error recovery.',
|
|
54
|
+
audiences: ['Application developers', 'Platform engineers'],
|
|
55
|
+
outcomes: ['Send a first event', 'Verify webhooks', 'Recover from API errors'],
|
|
56
|
+
terminology: { project: 'Pulse API workspace' },
|
|
57
|
+
exclusions: ['Dashboard administration and internal operations'],
|
|
58
|
+
instructions: 'Use runnable curl examples and explain observable success states.',
|
|
59
|
+
experienceLevel: 'beginner',
|
|
60
|
+
preferredExamples: ['curl', 'TypeScript'],
|
|
61
|
+
estimatedEffort: 'medium',
|
|
62
|
+
estimatedPages: pageDefinitions.length,
|
|
63
|
+
capabilities: pageDefinitions.map((page) => ({ id: `capability-${page.id}`, title: page.title, kind: page.type, evidence: page.evidenceDetails, pageIds: [page.id], disposition: 'planned' })),
|
|
64
|
+
navigation: { top: ['Documentation'], sections: [{ id: 'getting-started', title: 'Get started', pageIds: pageDefinitions.slice(0, 2).map((page) => page.id) }, { id: 'guides', title: 'Guides', pageIds: pageDefinitions.slice(2, 5).map((page) => page.id) }, { id: 'reference', title: 'Reference', pageIds: pageDefinitions.slice(5).map((page) => page.id) }] },
|
|
65
|
+
pages: pageDefinitions,
|
|
66
|
+
questions: [],
|
|
67
|
+
}, 'codex');
|
|
68
|
+
const approved = await approveDocumentationPlan(root, ready.id);
|
|
69
|
+
const generated = { ...approved, status: 'generated', updatedAt: new Date().toISOString() };
|
|
70
|
+
await writeFile(join(root, '.doxloop', 'plans', generated.id, 'plan.json'), `${JSON.stringify(generated, null, 2)}\n`, 'utf8');
|
|
71
|
+
await writeFile(join(root, '.doxloop', 'documentation-plan.json'), `${JSON.stringify(generated, null, 2)}\n`, 'utf8');
|
|
72
|
+
await writeEvidenceMap(root, { schemaVersion: 1, pages: Object.fromEntries(Object.keys(DEMO_PAGES).map((path) => [path, { sources: [{ source: 'pulse-api', operations: [DEMO_EVIDENCE[path]] }], confidence: 'verified', verifiedOn: { 'pulse-api': new Date().toISOString() } }])) });
|
|
73
|
+
await recordSyncState(root, [{ name: 'pulse-api', path: '../product/openapi.json', kind: 'openapi' }]);
|
|
74
|
+
const review = await persistReviewReport(root, '<doxloop-review>{"score":96,"hardGates":"pass","summary":"The bundled documentation is complete, navigable, evidence-backed, and ready to preview.","findings":[]}</doxloop-review>', { agent: 'codex', model: 'demo-bundled', reasoning: 'low' });
|
|
75
|
+
const validation = await validateProject(root);
|
|
76
|
+
if (validation.errors > 0) {
|
|
77
|
+
await rm(parent, { recursive: true, force: true });
|
|
78
|
+
throw new DoxloopError(`The bundled demo failed its own validation with ${validation.errors} errors.`);
|
|
79
|
+
}
|
|
80
|
+
return { parent, root, plan: generated, review, validation, cleanup: () => rm(parent, { recursive: true, force: true }) };
|
|
81
|
+
}
|
|
82
|
+
const DEMO_OPENAPI = {
|
|
83
|
+
openapi: '3.1.0',
|
|
84
|
+
info: { title: 'Pulse API', version: '1.0.0', description: 'Ingest product events and deliver signed webhooks.' },
|
|
85
|
+
servers: [{ url: 'https://api.pulse.example/v1' }],
|
|
86
|
+
components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer' } }, schemas: { Event: { type: 'object', required: ['type', 'data'], properties: { type: { type: 'string' }, data: { type: 'object' } } } } },
|
|
87
|
+
security: [{ bearerAuth: [] }],
|
|
88
|
+
paths: {
|
|
89
|
+
'/events': { post: { operationId: 'createEvent', summary: 'Send an event', responses: { '202': { description: 'Accepted' }, '400': { description: 'Invalid event' }, '401': { description: 'Unauthorized' } } } },
|
|
90
|
+
'/webhooks': { get: { operationId: 'listWebhooks', summary: 'List webhook endpoints', responses: { '200': { description: 'Webhook endpoints' } } }, post: { operationId: 'createWebhook', summary: 'Create a webhook endpoint', responses: { '201': { description: 'Created' } } } },
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const DEMO_TITLES = {
|
|
94
|
+
'index.mdx': 'Pulse API overview', 'quickstart.mdx': 'Quickstart', 'guides/authentication.mdx': 'Authentication', 'guides/send-events.mdx': 'Send events', 'guides/webhooks.mdx': 'Receive webhooks', 'reference/events.mdx': 'Events API', 'troubleshooting.mdx': 'Troubleshooting',
|
|
95
|
+
};
|
|
96
|
+
const DEMO_PURPOSES = {
|
|
97
|
+
'index.mdx': 'understand the API and choose a workflow', 'quickstart.mdx': 'send a first successful event', 'guides/authentication.mdx': 'authenticate requests safely', 'guides/send-events.mdx': 'construct and submit event payloads', 'guides/webhooks.mdx': 'configure and verify webhook delivery', 'reference/events.mdx': 'look up request and response behavior', 'troubleshooting.mdx': 'diagnose common request failures',
|
|
98
|
+
};
|
|
99
|
+
const DEMO_EVIDENCE = {
|
|
100
|
+
'index.mdx': 'POST /events', 'quickstart.mdx': 'POST /events', 'guides/authentication.mdx': 'bearerAuth', 'guides/send-events.mdx': 'POST /events', 'guides/webhooks.mdx': 'POST /webhooks', 'reference/events.mdx': 'POST /events', 'troubleshooting.mdx': 'POST /events 400/401',
|
|
101
|
+
};
|
|
102
|
+
function page(title, description, body) {
|
|
103
|
+
return `---\ntitle: ${JSON.stringify(title)}\ndescription: ${JSON.stringify(description)}\n---\n\n# ${title}\n\n${body.trim()}\n`;
|
|
104
|
+
}
|
|
105
|
+
const DEMO_PAGES = {
|
|
106
|
+
'index.mdx': page('Pulse API overview', 'Understand what Pulse API does and choose the right integration workflow.', 'Pulse API accepts product events and delivers them to signed webhook endpoints.\n\n- Start with the [quickstart](/quickstart).\n- Learn how [authentication](/guides/authentication) works.\n- Use the [Events API reference](/reference/events) while integrating.'),
|
|
107
|
+
'quickstart.mdx': page('Quickstart', 'Authenticate and send your first accepted event in a few minutes.', '## Send an event\n\n```bash\ncurl -X POST https://api.pulse.example/v1/events \\\n -H "Authorization: Bearer $PULSE_API_TOKEN" \\\n -H "Content-Type: application/json" \\\n -d \'{"type":"user.created","data":{"id":"usr_123"}}\'\n```\n\nA successful request returns HTTP `202 Accepted`. Continue with [webhooks](/guides/webhooks).'),
|
|
108
|
+
'guides/authentication.mdx': page('Authentication', 'Send API credentials safely with every Pulse API request.', 'Pulse API uses bearer authentication. Store the token in a secret manager and send it in the `Authorization` header.\n\n```http\nAuthorization: Bearer YOUR_TOKEN\n```\n\nNever place a token in a URL, source file, or client-side application.'),
|
|
109
|
+
'guides/send-events.mdx': page('Send events', 'Create valid event payloads and recognize an accepted request.', 'Send JSON to `POST /events` with a string `type` and an object `data`. The API returns `202` after accepting the event for processing.\n\nUse stable event names such as `user.created` so consumers can route them predictably.'),
|
|
110
|
+
'guides/webhooks.mdx': page('Receive webhooks', 'Create an endpoint and prepare an application to receive event deliveries.', 'Create a destination with `POST /webhooks`, then expose an HTTPS handler that returns a successful response promptly. List configured destinations with `GET /webhooks`.\n\nTest the complete path with a non-production event before enabling a live integration.'),
|
|
111
|
+
'reference/events.mdx': page('Events API', 'Look up the request contract and response behavior for event ingestion.', '## `POST /events`\n\nRequires bearer authentication and a JSON body containing `type` and `data`.\n\n| Status | Meaning |\n| --- | --- |\n| `202` | Event accepted |\n| `400` | Payload is invalid |\n| `401` | Authentication failed |'),
|
|
112
|
+
'troubleshooting.mdx': page('Troubleshooting', 'Resolve authentication and payload errors when sending Pulse API events.', '## `401 Unauthorized`\n\nConfirm that the token is present, active, and sent as a bearer token.\n\n## `400 Bad Request`\n\nConfirm that the body is JSON and includes both `type` and `data`. Compare the request with the [quickstart](/quickstart).'),
|
|
113
|
+
};
|
|
114
|
+
const DEMO_NAVIGATION = {
|
|
115
|
+
version: 1, name: 'Pulse API', description: 'Evidence-backed example documentation generated by Doxloop.',
|
|
116
|
+
spaces: [{ name: 'Documentation', slug: 'docs', icon: 'book', nav: [
|
|
117
|
+
{ type: 'group', label: 'Get started', icon: 'rocket', items: [{ type: 'page', file: 'index', title: 'Overview' }, { type: 'page', file: 'quickstart', title: 'Quickstart' }] },
|
|
118
|
+
{ type: 'group', label: 'Guides', icon: 'book', items: [{ type: 'page', file: 'guides/authentication', title: 'Authentication' }, { type: 'page', file: 'guides/send-events', title: 'Send events' }, { type: 'page', file: 'guides/webhooks', title: 'Receive webhooks' }] },
|
|
119
|
+
{ type: 'group', label: 'Reference', icon: 'api', items: [{ type: 'page', file: 'reference/events', title: 'Events API' }, { type: 'page', file: 'troubleshooting', title: 'Troubleshooting' }] },
|
|
120
|
+
] }], theme: { primaryColor: '#0f766e', mode: 'system', font: 'Inter', headingFont: 'Inter', codeFont: 'ui-monospace' },
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=demo.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DeployTargetId } from './deploy-targets/types.js';
|
|
2
|
+
type CredentialTarget = Extract<DeployTargetId, 'netlify' | 'vercel'>;
|
|
3
|
+
export declare function saveDeployCredential(target: CredentialTarget, token: string): Promise<void>;
|
|
4
|
+
export declare function deployCredential(target: CredentialTarget): Promise<string>;
|
|
5
|
+
export declare function deployCredentialState(): Promise<Record<CredentialTarget, boolean>>;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=deploy-credentials.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { DoxloopError } from './errors.js';
|
|
6
|
+
import { pathExists } from './fs.js';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
const execute = promisify(execFile);
|
|
9
|
+
const KEYCHAIN_SERVICE = 'com.doxloop.deploy';
|
|
10
|
+
export async function saveDeployCredential(target, token) {
|
|
11
|
+
const value = token.trim();
|
|
12
|
+
if (value.length < 8)
|
|
13
|
+
throw new DoxloopError(`${targetLabel(target)} token is too short.`);
|
|
14
|
+
if (process.platform === 'darwin') {
|
|
15
|
+
try {
|
|
16
|
+
await execute('security', ['add-generic-password', '-a', target, '-s', KEYCHAIN_SERVICE, '-w', value, '-U']);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
throw new DoxloopError(`Could not save the ${targetLabel(target)} token in macOS Keychain: ${error instanceof Error ? error.message : String(error)}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const path = credentialPath();
|
|
24
|
+
const current = await readCredentials();
|
|
25
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
26
|
+
await writeFile(path, `${JSON.stringify({ ...current, [target]: value }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
27
|
+
if (process.platform !== 'win32')
|
|
28
|
+
await chmod(path, 0o600);
|
|
29
|
+
}
|
|
30
|
+
export async function deployCredential(target) {
|
|
31
|
+
const environment = target === 'netlify'
|
|
32
|
+
? process.env.DOXLOOP_NETLIFY_TOKEN ?? process.env.NETLIFY_AUTH_TOKEN
|
|
33
|
+
: process.env.DOXLOOP_VERCEL_TOKEN ?? process.env.VERCEL_TOKEN;
|
|
34
|
+
const token = environment ?? await storedCredential(target);
|
|
35
|
+
if (!token) {
|
|
36
|
+
throw new DoxloopError(`No ${targetLabel(target)} token is configured. Set ${target === 'netlify' ? 'DOXLOOP_NETLIFY_TOKEN' : 'DOXLOOP_VERCEL_TOKEN'} or save one from the Deploy page.`);
|
|
37
|
+
}
|
|
38
|
+
return token;
|
|
39
|
+
}
|
|
40
|
+
export async function deployCredentialState() {
|
|
41
|
+
const [netlify, vercel] = await Promise.all([storedCredential('netlify'), storedCredential('vercel')]);
|
|
42
|
+
return {
|
|
43
|
+
netlify: Boolean(process.env.DOXLOOP_NETLIFY_TOKEN ?? process.env.NETLIFY_AUTH_TOKEN ?? netlify),
|
|
44
|
+
vercel: Boolean(process.env.DOXLOOP_VERCEL_TOKEN ?? process.env.VERCEL_TOKEN ?? vercel),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function storedCredential(target) {
|
|
48
|
+
if (process.platform === 'darwin') {
|
|
49
|
+
try {
|
|
50
|
+
const result = await execute('security', ['find-generic-password', '-a', target, '-s', KEYCHAIN_SERVICE, '-w']);
|
|
51
|
+
return result.stdout.trim() || undefined;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return (await readCredentials())[target];
|
|
58
|
+
}
|
|
59
|
+
async function readCredentials() {
|
|
60
|
+
const path = credentialPath();
|
|
61
|
+
if (!(await pathExists(path)))
|
|
62
|
+
return {};
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new DoxloopError(`Cannot read deployment credentials at ${path}.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function credentialPath() {
|
|
71
|
+
return process.env.DOXLOOP_CONFIG_HOME
|
|
72
|
+
? join(process.env.DOXLOOP_CONFIG_HOME, 'deploy-targets.json')
|
|
73
|
+
: join(homedir(), '.config', 'doxloop', 'deploy-targets.json');
|
|
74
|
+
}
|
|
75
|
+
function targetLabel(target) { return target === 'netlify' ? 'Netlify' : 'Vercel'; }
|
|
76
|
+
//# sourceMappingURL=deploy-credentials.js.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { cp, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { DoxloopError } from '../errors.js';
|
|
7
|
+
const execute = promisify(execFile);
|
|
8
|
+
export const githubPagesTarget = {
|
|
9
|
+
id: 'github-pages',
|
|
10
|
+
label: 'GitHub Pages',
|
|
11
|
+
async configure(options) {
|
|
12
|
+
const branch = options.branch?.trim() || 'gh-pages';
|
|
13
|
+
if (!(branch === 'gh-pages' || /^doxloop-pages\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(branch)) || branch.includes('..')) {
|
|
14
|
+
throw new DoxloopError('Use gh-pages or doxloop-pages/<site> as a deployment-only branch. Source branches cannot be deployment targets.');
|
|
15
|
+
}
|
|
16
|
+
await git(options.root, ['rev-parse', '--is-inside-work-tree']);
|
|
17
|
+
await git(options.root, ['check-ref-format', '--branch', branch]);
|
|
18
|
+
const current = (await git(options.root, ['branch', '--show-current'])).trim();
|
|
19
|
+
if (current === branch)
|
|
20
|
+
throw new DoxloopError('The deployment branch is checked out. Switch to your documentation source branch before publishing.');
|
|
21
|
+
const remote = (await git(options.root, ['remote', 'get-url', 'origin'])).trim();
|
|
22
|
+
const withBase = { ...options, branch, ...(options.basePath ? {} : inferredBasePath(remote)) };
|
|
23
|
+
const siteUrl = githubPagesUrl(remote, withBase.basePath);
|
|
24
|
+
return { ...withBase, ...(siteUrl ? { siteUrl } : {}) };
|
|
25
|
+
},
|
|
26
|
+
async publish(bundle, rawOptions) {
|
|
27
|
+
const options = await this.configure(rawOptions);
|
|
28
|
+
const remote = (await git(options.root, ['remote', 'get-url', 'origin'])).trim();
|
|
29
|
+
const parent = await mkdtemp(join(tmpdir(), 'doxloop-pages-'));
|
|
30
|
+
const checkout = join(parent, 'site');
|
|
31
|
+
try {
|
|
32
|
+
await git(parent, ['clone', '--no-checkout', options.root, checkout]);
|
|
33
|
+
await git(checkout, ['remote', 'set-url', 'origin', remote]);
|
|
34
|
+
const ref = `refs/heads/${options.branch}`;
|
|
35
|
+
const advertised = (await git(checkout, ['ls-remote', '--heads', 'origin', ref])).trim().split(/\s+/)[0] || '';
|
|
36
|
+
if (advertised) {
|
|
37
|
+
await git(checkout, ['fetch', '--no-tags', 'origin', ref]);
|
|
38
|
+
const fetched = (await git(checkout, ['rev-parse', 'FETCH_HEAD'])).trim();
|
|
39
|
+
if (fetched !== advertised)
|
|
40
|
+
throw new DoxloopError('The deployment branch changed during preparation. Retry publishing.');
|
|
41
|
+
const marker = await git(checkout, ['show', `${fetched}:.doxloop-deployment.json`]).catch(() => '');
|
|
42
|
+
if (marker.trim() !== '{"schemaVersion":1,"owner":"doxloop-static-deploy"}')
|
|
43
|
+
throw new DoxloopError('The remote branch is not owned by Doxloop static deployments. Choose a new doxloop-pages/<site> branch to preserve its existing content.');
|
|
44
|
+
await git(checkout, ['checkout', '--detach', fetched]);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
await git(checkout, ['checkout', '--orphan', `doxloop-publish-${Date.now()}`]);
|
|
48
|
+
}
|
|
49
|
+
for (const entry of await readdir(checkout)) {
|
|
50
|
+
if (entry !== '.git')
|
|
51
|
+
await rm(join(checkout, entry), { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
for (const entry of await readdir(bundle.outputDir)) {
|
|
54
|
+
await cp(join(bundle.outputDir, entry), join(checkout, entry), { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
await writeFile(join(checkout, '.nojekyll'), '', 'utf8');
|
|
57
|
+
await writeFile(join(checkout, '.doxloop-deployment.json'), '{"schemaVersion":1,"owner":"doxloop-static-deploy"}\n', 'utf8');
|
|
58
|
+
await git(checkout, ['add', '--all']);
|
|
59
|
+
await git(checkout, ['-c', 'user.name=Doxloop', '-c', 'user.email=doxloop@local', 'commit', '--allow-empty', '-m', 'docs: publish static site']);
|
|
60
|
+
const commit = (await git(checkout, ['rev-parse', 'HEAD'])).trim();
|
|
61
|
+
// Existing deployments retain their history. An explicit lease rejects concurrent updates.
|
|
62
|
+
await git(checkout, ['push', `--force-with-lease=${ref}:${advertised}`, 'origin', `HEAD:${ref}`]);
|
|
63
|
+
const url = githubPagesUrl(remote, options.basePath);
|
|
64
|
+
return {
|
|
65
|
+
id: commit,
|
|
66
|
+
...(url ? { url } : {}),
|
|
67
|
+
detail: `Pushed ${bundle.files} files to ${options.branch}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await rm(parent, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
async function git(cwd, args) {
|
|
76
|
+
try {
|
|
77
|
+
return (await execute('git', args, { cwd, maxBuffer: 4_000_000 })).stdout;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
throw new DoxloopError(`GitHub Pages publish failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function repository(remote) {
|
|
84
|
+
const match = /github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?$/.exec(remote);
|
|
85
|
+
return match ? { owner: match[1], name: match[2] } : undefined;
|
|
86
|
+
}
|
|
87
|
+
function inferredBasePath(remote) {
|
|
88
|
+
const repo = repository(remote);
|
|
89
|
+
if (!repo || repo.name.toLowerCase() === `${repo.owner.toLowerCase()}.github.io`)
|
|
90
|
+
return {};
|
|
91
|
+
return { basePath: `/${repo.name}` };
|
|
92
|
+
}
|
|
93
|
+
function githubPagesUrl(remote, basePath) {
|
|
94
|
+
const repo = repository(remote);
|
|
95
|
+
if (!repo)
|
|
96
|
+
return undefined;
|
|
97
|
+
const path = basePath?.replace(/^\/+|\/+$/g, '');
|
|
98
|
+
return `https://${repo.owner.toLowerCase()}.github.io/${path ? `${path}/` : ''}`;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=github-pages.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DeployTarget, DeployTargetId, DeployTargetOptions, DeployTargetResult } from './types.js';
|
|
2
|
+
export type { DeployBundle, DeployTarget, DeployTargetId, DeployTargetOptions, DeployTargetResult } from './types.js';
|
|
3
|
+
export declare const STATIC_DEPLOY_TARGETS: readonly DeployTarget[];
|
|
4
|
+
export declare function deployTarget(id: DeployTargetId): DeployTarget;
|
|
5
|
+
export declare function publishStaticTarget(id: Extract<DeployTargetId, 'github-pages' | 'netlify' | 'vercel'>, options: DeployTargetOptions & {
|
|
6
|
+
dryRun?: boolean;
|
|
7
|
+
}): Promise<DeployTargetResult>;
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { packageStaticOutput, containedOutput, runLocalBuild, writeDryRunArchive } from '../artifact-deploy.js';
|
|
5
|
+
import { buildDoxbrixStaticSite } from '../doxbrix-build.js';
|
|
6
|
+
import { DoxloopError } from '../errors.js';
|
|
7
|
+
import { loadGeneratorAdapter } from '../generators.js';
|
|
8
|
+
import { recordDeployment } from '../history.js';
|
|
9
|
+
import { loadProject } from '../project.js';
|
|
10
|
+
import { validateProject } from '../validation.js';
|
|
11
|
+
import { githubPagesTarget } from './github-pages.js';
|
|
12
|
+
import { netlifyTarget } from './netlify.js';
|
|
13
|
+
import { vercelTarget } from './vercel.js';
|
|
14
|
+
export const STATIC_DEPLOY_TARGETS = [githubPagesTarget, netlifyTarget, vercelTarget];
|
|
15
|
+
export function deployTarget(id) {
|
|
16
|
+
const target = STATIC_DEPLOY_TARGETS.find((candidate) => candidate.id === id);
|
|
17
|
+
if (!target)
|
|
18
|
+
throw new DoxloopError(`Unsupported static deploy target: ${id}`);
|
|
19
|
+
return target;
|
|
20
|
+
}
|
|
21
|
+
export async function publishStaticTarget(id, options) {
|
|
22
|
+
const startedAt = new Date().toISOString();
|
|
23
|
+
const target = deployTarget(id);
|
|
24
|
+
let configured;
|
|
25
|
+
try {
|
|
26
|
+
const validation = await validateProject(options.root);
|
|
27
|
+
if (validation.errors > 0)
|
|
28
|
+
throw new DoxloopError(`Deployment stopped because documentation has ${validation.errors} validation error${validation.errors === 1 ? '' : 's'}. Run \`doxloop test\`.`);
|
|
29
|
+
configured = await target.configure(options);
|
|
30
|
+
const project = await loadProject(options.root);
|
|
31
|
+
const temporary = await mkdtemp(join(tmpdir(), 'doxloop-deploy-'));
|
|
32
|
+
try {
|
|
33
|
+
let outputDir;
|
|
34
|
+
if (project.generator === 'doxbrix') {
|
|
35
|
+
outputDir = join(temporary, 'site');
|
|
36
|
+
await buildDoxbrixStaticSite({
|
|
37
|
+
root: options.root,
|
|
38
|
+
outDir: outputDir,
|
|
39
|
+
...(configured.basePath ? { basePath: configured.basePath } : {}),
|
|
40
|
+
...(configured.siteUrl ? { siteUrl: configured.siteUrl } : {}),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const adapter = await loadGeneratorAdapter(options.root, project);
|
|
45
|
+
await runLocalBuild(adapter.id, adapter.build.command, options.root, configured.siteUrl ?? 'https://example.com');
|
|
46
|
+
outputDir = containedOutput(options.root, adapter.build.outputDir);
|
|
47
|
+
}
|
|
48
|
+
const packaged = await packageStaticOutput(outputDir);
|
|
49
|
+
const bundle = { outputDir, archive: packaged.archive, files: packaged.files, bytes: packaged.archive.byteLength, sha256: packaged.sha256 };
|
|
50
|
+
if (options.dryRun) {
|
|
51
|
+
const archivePath = await writeDryRunArchive(options.root, bundle.archive);
|
|
52
|
+
process.stdout.write(`Deployment is valid.\nTarget: ${target.label}\nFiles: ${bundle.files}\nArchive: ${archivePath}\nSHA-256: ${bundle.sha256}\nNo data was uploaded.\n`);
|
|
53
|
+
return { detail: archivePath };
|
|
54
|
+
}
|
|
55
|
+
const result = await target.publish(bundle, configured);
|
|
56
|
+
await recordDeployment(options.root, {
|
|
57
|
+
target: id,
|
|
58
|
+
status: 'succeeded',
|
|
59
|
+
startedAt,
|
|
60
|
+
name: options.name,
|
|
61
|
+
slug: options.slug,
|
|
62
|
+
...(result.url ? { url: result.url } : {}),
|
|
63
|
+
pagesCount: validation.pages.length,
|
|
64
|
+
bytes: bundle.bytes,
|
|
65
|
+
});
|
|
66
|
+
process.stdout.write(`${target.label} deployment succeeded.${result.detail ? ` ${result.detail}.` : ''}${result.url ? `\n${result.url}` : ''}\n`);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
await rm(temporary, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
await recordDeployment(options.root, { target: id, status: 'failed', startedAt, name: options.name, slug: options.slug, error: error instanceof Error ? error.message : String(error) });
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { deployCredential } from '../deploy-credentials.js';
|
|
2
|
+
import { DoxloopError } from '../errors.js';
|
|
3
|
+
export const netlifyTarget = {
|
|
4
|
+
id: 'netlify',
|
|
5
|
+
label: 'Netlify',
|
|
6
|
+
async configure(options) {
|
|
7
|
+
if (!options.siteId?.trim())
|
|
8
|
+
throw new DoxloopError('Netlify requires a site ID.');
|
|
9
|
+
return { ...options, siteId: options.siteId.trim(), apiUrl: apiOrigin(options.apiUrl, 'https://api.netlify.com') };
|
|
10
|
+
},
|
|
11
|
+
async publish(bundle, rawOptions) {
|
|
12
|
+
const options = await this.configure(rawOptions);
|
|
13
|
+
const token = await deployCredential('netlify');
|
|
14
|
+
const response = await fetch(`${options.apiUrl}/api/v1/sites/${encodeURIComponent(options.siteId)}/deploys`, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/zip' },
|
|
17
|
+
body: bundle.archive,
|
|
18
|
+
redirect: 'error',
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok)
|
|
21
|
+
throw await providerError('Netlify', response);
|
|
22
|
+
const result = await response.json();
|
|
23
|
+
return { ...(result.id ? { id: result.id } : {}), ...(result.deploy_ssl_url ?? result.ssl_url ?? result.url ? { url: result.deploy_ssl_url ?? result.ssl_url ?? result.url } : {}), detail: result.state ? `Deploy ${result.state}` : 'Deploy uploaded' };
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
function apiOrigin(value, fallback) {
|
|
27
|
+
const raw = value?.trim() || fallback;
|
|
28
|
+
let url;
|
|
29
|
+
try {
|
|
30
|
+
url = new URL(raw);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new DoxloopError(`Invalid Netlify API URL: ${raw}`);
|
|
34
|
+
}
|
|
35
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '::1'].includes(url.hostname)))
|
|
36
|
+
throw new DoxloopError('Netlify API URL must use HTTPS.');
|
|
37
|
+
if (url.username || url.password || url.search || url.hash)
|
|
38
|
+
throw new DoxloopError('Netlify API URL cannot contain credentials, query, or fragment.');
|
|
39
|
+
return url.toString().replace(/\/+$/, '');
|
|
40
|
+
}
|
|
41
|
+
async function providerError(provider, response) {
|
|
42
|
+
let detail = `${response.status} ${response.statusText}`.trim();
|
|
43
|
+
try {
|
|
44
|
+
detail = (await response.text()).trim().slice(0, 1_000) || detail;
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
return new DoxloopError(`${provider} deployment failed: ${detail}`);
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=netlify.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type DeployTargetId = 'doxbrix' | 'export' | 'github-pages' | 'netlify' | 'vercel';
|
|
2
|
+
export interface DeployBundle {
|
|
3
|
+
outputDir: string;
|
|
4
|
+
archive: Uint8Array;
|
|
5
|
+
files: number;
|
|
6
|
+
bytes: number;
|
|
7
|
+
sha256: string;
|
|
8
|
+
}
|
|
9
|
+
export interface DeployTargetOptions {
|
|
10
|
+
root: string;
|
|
11
|
+
name: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
apiUrl?: string;
|
|
14
|
+
siteId?: string;
|
|
15
|
+
projectId?: string;
|
|
16
|
+
teamId?: string;
|
|
17
|
+
branch?: string;
|
|
18
|
+
basePath?: string;
|
|
19
|
+
siteUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface DeployTargetResult {
|
|
22
|
+
url?: string;
|
|
23
|
+
id?: string;
|
|
24
|
+
detail?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface DeployTarget {
|
|
27
|
+
id: DeployTargetId;
|
|
28
|
+
label: string;
|
|
29
|
+
configure(options: DeployTargetOptions): Promise<DeployTargetOptions>;
|
|
30
|
+
publish(bundle: DeployBundle, options: DeployTargetOptions): Promise<DeployTargetResult>;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { deployCredential } from '../deploy-credentials.js';
|
|
5
|
+
import { DoxloopError } from '../errors.js';
|
|
6
|
+
export const vercelTarget = {
|
|
7
|
+
id: 'vercel',
|
|
8
|
+
label: 'Vercel',
|
|
9
|
+
async configure(options) {
|
|
10
|
+
if (!options.projectId?.trim())
|
|
11
|
+
throw new DoxloopError('Vercel requires a project ID or project name.');
|
|
12
|
+
return { ...options, projectId: options.projectId.trim(), apiUrl: apiOrigin(options.apiUrl) };
|
|
13
|
+
},
|
|
14
|
+
async publish(bundle, rawOptions) {
|
|
15
|
+
const options = await this.configure(rawOptions);
|
|
16
|
+
const token = await deployCredential('vercel');
|
|
17
|
+
const files = await deploymentFiles(bundle.outputDir);
|
|
18
|
+
const query = options.teamId ? `?teamId=${encodeURIComponent(options.teamId)}` : '';
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
const upload = await fetch(`${options.apiUrl}/v2/files${query}`, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: {
|
|
23
|
+
Authorization: `Bearer ${token}`,
|
|
24
|
+
'Content-Type': 'application/octet-stream',
|
|
25
|
+
'Content-Length': String(file.data.byteLength),
|
|
26
|
+
'x-vercel-digest': file.sha,
|
|
27
|
+
},
|
|
28
|
+
body: file.data,
|
|
29
|
+
redirect: 'error',
|
|
30
|
+
});
|
|
31
|
+
if (!upload.ok)
|
|
32
|
+
throw await providerError(upload);
|
|
33
|
+
}
|
|
34
|
+
const response = await fetch(`${options.apiUrl}/v13/deployments${query}`, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
name: options.projectId,
|
|
39
|
+
project: options.projectId,
|
|
40
|
+
target: 'production',
|
|
41
|
+
files: files.map(({ file, sha, size }) => ({ file, sha, size })),
|
|
42
|
+
projectSettings: { framework: null },
|
|
43
|
+
}),
|
|
44
|
+
redirect: 'error',
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok)
|
|
47
|
+
throw await providerError(response);
|
|
48
|
+
const result = await response.json();
|
|
49
|
+
const hostname = result.alias?.[0] ?? result.url;
|
|
50
|
+
return { ...(result.id ? { id: result.id } : {}), ...(hostname ? { url: `https://${hostname.replace(/^https?:\/\//, '')}` } : {}), detail: result.readyState ? `Deploy ${result.readyState.toLowerCase()}` : 'Deploy created' };
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
async function deploymentFiles(root) {
|
|
54
|
+
const files = [];
|
|
55
|
+
const visit = async (directory) => {
|
|
56
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
57
|
+
const absolute = resolve(directory, entry.name);
|
|
58
|
+
const stat = await lstat(absolute);
|
|
59
|
+
if (stat.isSymbolicLink())
|
|
60
|
+
throw new DoxloopError('Vercel deployment output cannot contain symbolic links.');
|
|
61
|
+
if (stat.isDirectory())
|
|
62
|
+
await visit(absolute);
|
|
63
|
+
else if (stat.isFile()) {
|
|
64
|
+
const data = new Uint8Array(await readFile(absolute));
|
|
65
|
+
files.push({ file: relative(root, absolute).split(sep).join('/'), data, sha: createHash('sha1').update(data).digest('hex'), size: data.byteLength });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
await visit(root);
|
|
70
|
+
return files;
|
|
71
|
+
}
|
|
72
|
+
function apiOrigin(value) {
|
|
73
|
+
const raw = value?.trim() || 'https://api.vercel.com';
|
|
74
|
+
let url;
|
|
75
|
+
try {
|
|
76
|
+
url = new URL(raw);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new DoxloopError(`Invalid Vercel API URL: ${raw}`);
|
|
80
|
+
}
|
|
81
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '::1'].includes(url.hostname)))
|
|
82
|
+
throw new DoxloopError('Vercel API URL must use HTTPS.');
|
|
83
|
+
if (url.username || url.password || url.search || url.hash)
|
|
84
|
+
throw new DoxloopError('Vercel API URL cannot contain credentials, query, or fragment.');
|
|
85
|
+
return url.toString().replace(/\/+$/, '');
|
|
86
|
+
}
|
|
87
|
+
async function providerError(response) {
|
|
88
|
+
let detail = `${response.status} ${response.statusText}`.trim();
|
|
89
|
+
try {
|
|
90
|
+
detail = (await response.text()).trim().slice(0, 1_000) || detail;
|
|
91
|
+
}
|
|
92
|
+
catch { }
|
|
93
|
+
return new DoxloopError(`Vercel deployment failed: ${detail}`);
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=vercel.js.map
|
package/dist/deploy.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ReaderVerificationMetadata } from './types.js';
|
|
2
|
+
import type { DeploymentTargetSetting } from './types.js';
|
|
1
3
|
interface DeploymentBundle {
|
|
2
4
|
manifest: unknown;
|
|
3
5
|
basePath: string;
|
|
@@ -9,14 +11,21 @@ interface DeploymentBundle {
|
|
|
9
11
|
path: string;
|
|
10
12
|
base64: string;
|
|
11
13
|
}>;
|
|
14
|
+
verification?: ReaderVerificationMetadata;
|
|
12
15
|
}
|
|
13
16
|
export declare function deploy(options: {
|
|
14
17
|
root: string;
|
|
18
|
+
target?: DeploymentTargetSetting;
|
|
15
19
|
name?: string;
|
|
16
20
|
slug?: string;
|
|
17
21
|
dryRun?: boolean;
|
|
18
22
|
public?: boolean;
|
|
19
23
|
apiUrl?: string;
|
|
24
|
+
siteId?: string;
|
|
25
|
+
projectId?: string;
|
|
26
|
+
teamId?: string;
|
|
27
|
+
branch?: string;
|
|
28
|
+
basePath?: string;
|
|
20
29
|
}): Promise<void>;
|
|
21
30
|
export declare function buildDeploymentBundle(root: string, contentDir: string): Promise<DeploymentBundle>;
|
|
22
31
|
export {};
|