@agentskit/doc-bridge 1.2.6 → 1.3.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 +8 -0
- package/README.md +9 -3
- package/action.yml +1 -1
- package/dist/cli/program.js +344 -130
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +3 -1
- package/dist/config/index.js.map +1 -1
- package/dist/{index-DGI9TBLE.d.ts → index-DAeq_OIi.d.ts} +22 -22
- package/dist/index.d.ts +7 -4
- package/dist/index.js +320 -105
- package/dist/index.js.map +1 -1
- package/docs/POSITIONING.md +1 -1
- package/docs/examples.md +4 -0
- package/docs/getting-started.md +1 -1
- package/docs/landing/index.html +1 -1
- package/docs/qa/vitepress-starlight-adapters.md +19 -0
- package/docs/spec/config-v1.md +33 -2
- package/examples/nextra-only.config.ts +17 -0
- package/examples/nx-monorepo.config.ts +11 -0
- package/examples/starlight-only.config.ts +17 -0
- package/examples/vitepress-only.config.ts +19 -0
- package/mcpb/manifest.json +1 -1
- package/package.json +4 -2
- package/src/config/schema.ts +3 -1
- package/src/index-builder/build-handoffs.ts +2 -0
- package/src/index-builder/build-index.ts +7 -1
- package/src/index-builder/human-adapters/index.ts +6 -0
- package/src/index-builder/human-adapters/nextra.ts +43 -0
- package/src/index-builder/human-adapters/starlight.ts +40 -0
- package/src/index-builder/human-adapters/vitepress.ts +43 -0
- package/src/index-builder/plugins/nx.ts +161 -0
- package/src/index-builder/plugins/pnpm-monorepo.ts +1 -0
- package/src/index-builder/watch-index.ts +11 -2
- package/src/index.ts +1 -0
- package/src/version.ts +1 -1
package/docs/POSITIONING.md
CHANGED
|
@@ -70,7 +70,7 @@ Engineering teams with real ownership (monorepos first). Secondary: solo libs, i
|
|
|
70
70
|
|
|
71
71
|
```
|
|
72
72
|
required: index, CLI, MCP handoff tools, gate presets
|
|
73
|
-
optional: fumadocs | docusaurus | plain-markdown plugins
|
|
73
|
+
optional: fumadocs | docusaurus | vitepress | starlight | nextra | plain-markdown plugins
|
|
74
74
|
optional: memory ingest → promote
|
|
75
75
|
optional: @agentskit/rag + ink chat (intelligence.*)
|
|
76
76
|
optional: Playbook / Registry federation
|
package/docs/examples.md
CHANGED
|
@@ -11,8 +11,12 @@ Config sketches under [`examples/`](../examples/):
|
|
|
11
11
|
|------|---------|
|
|
12
12
|
| `minimal-plain-markdown.config.ts` | Solo markdown, Layer 0 only |
|
|
13
13
|
| `pnpm-monorepo.config.ts` | Workspace discovery + ownership |
|
|
14
|
+
| `nx-monorepo.config.ts` | Static Nx project discovery + inferred checks |
|
|
14
15
|
| `fumadocs-only.config.ts` | Human bridge via Fumadocs |
|
|
15
16
|
| `docusaurus-only.config.ts` | Human bridge via Docusaurus |
|
|
17
|
+
| `vitepress-only.config.ts` | Human bridge via VitePress |
|
|
18
|
+
| `starlight-only.config.ts` | Human bridge via Astro Starlight |
|
|
19
|
+
| `nextra-only.config.ts` | Human bridge via Nextra |
|
|
16
20
|
| `fumadocs-with-chat.config.ts` | Standard + intelligence (AgentsKit peers) |
|
|
17
21
|
| `docusaurus-with-memory.config.ts` | Assisted memory promotion path |
|
|
18
22
|
|
package/docs/getting-started.md
CHANGED
|
@@ -105,7 +105,7 @@ Tools: `handoff.resolve`, `doc.search`, `doc.get`, `gate.status`, …
|
|
|
105
105
|
## Human ↔ agent bridge
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
|
-
# After configuring corpus.human (fumadocs | docusaurus | plain-markdown)
|
|
108
|
+
# After configuring corpus.human (fumadocs | docusaurus | vitepress | starlight | nextra | plain-markdown)
|
|
109
109
|
ak-docs index
|
|
110
110
|
ak-docs query package <id> --agent # includes humanDoc when linked
|
|
111
111
|
ak-docs gate run human-guide-links
|
package/docs/landing/index.html
CHANGED
|
@@ -272,7 +272,7 @@
|
|
|
272
272
|
<tr><td>CLI</td><td>Query ownership, inspect handoffs, run doctor, search docs.</td><td><code>ak-docs query package auth --agent</code></td></tr>
|
|
273
273
|
<tr><td>MCP</td><td>Lets Cursor, Claude Code, and Codex-style agents resolve before editing.</td><td><code>ak-docs mcp install --cursor</code></td></tr>
|
|
274
274
|
<tr><td>CI</td><td>Blocks stale indexes and broken human-doc bridges.</td><td><code>ak-docs index && ak-docs gate run</code></td></tr>
|
|
275
|
-
<tr><td>Adapters</td><td>Connect Fumadocs, Docusaurus, plain markdown, and pnpm workspaces.</td><td><code>fumadocs</code> · <code>docusaurus</code> · <code>plain-markdown</code></td></tr>
|
|
275
|
+
<tr><td>Adapters</td><td>Connect Fumadocs, Docusaurus, VitePress, Starlight, Nextra, plain markdown, and pnpm workspaces.</td><td><code>fumadocs</code> · <code>docusaurus</code> · <code>vitepress</code> · <code>starlight</code> · <code>nextra</code> · <code>plain-markdown</code></td></tr>
|
|
276
276
|
<tr><td>Memory pipeline</td><td>Classifies local agent memory and drafts documentation updates.</td><td><code>ak-docs memory promote --pr</code></td></tr>
|
|
277
277
|
<tr><td>Optional RAG/chat</td><td>Adds handoff-first chat when you install AgentsKit peers.</td><td><code>ak-docs rag ingest && ak-docs chat</code></td></tr>
|
|
278
278
|
</tbody>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# VitePress and Starlight adapter QA plan
|
|
2
|
+
|
|
3
|
+
This plan records the local acceptance criteria for the first-party VitePress and Astro Starlight human-documentation adapters.
|
|
4
|
+
|
|
5
|
+
## Contract checks
|
|
6
|
+
|
|
7
|
+
- A VitePress corpus scans Markdown and MDX below its configured documentation root, maps `index` files to their directory route, follows the framework's default `.html` routes or optional `cleanUrls`, applies declarative `srcExclude` globs, respects an optional `urlPrefix`, and keeps Doc Bridge join metadata (`package`, `module`, or `id`) stable.
|
|
8
|
+
- A Starlight corpus scans Markdown and MDX below its configured content root, uses Astro's optional `slug` frontmatter for the public route, maps index pages correctly, respects an optional `urlPrefix`, and keeps Doc Bridge join metadata stable.
|
|
9
|
+
- Both adapters reject roots outside the project through the shared bounded, containment-aware scanner.
|
|
10
|
+
- Neither adapter executes framework configuration or user JavaScript.
|
|
11
|
+
- Existing Fumadocs, Docusaurus, and plain-Markdown behavior remains unchanged.
|
|
12
|
+
|
|
13
|
+
## Local verification
|
|
14
|
+
|
|
15
|
+
1. Add focused fixtures for default routes, index routes, metadata overrides, URL prefixes, and nested agent-corpus exclusion.
|
|
16
|
+
2. Run `pnpm vitest run tests/human-adapters.test.ts tests/schemas.test.ts`.
|
|
17
|
+
3. Run `pnpm typecheck` and `pnpm build` to validate the public configuration type and emitted declarations.
|
|
18
|
+
4. Run the complete `pnpm test` suite to detect regressions.
|
|
19
|
+
5. Run `git diff --check` and inspect the final diff for scope and generated-file noise.
|
package/docs/spec/config-v1.md
CHANGED
|
@@ -129,10 +129,34 @@ type HumanCorpusPluginId =
|
|
|
129
129
|
| 'fumadocs' // markdown scan with index routes, (group) slugs, pages allowlists
|
|
130
130
|
| 'docusaurus' // markdown scan with id/slug frontmatter + static sidebars.js
|
|
131
131
|
| 'mkdocs' // planned
|
|
132
|
-
| 'vitepress' //
|
|
132
|
+
| 'vitepress' // file routes, srcDir/docsDir, optional cleanUrls
|
|
133
|
+
| 'starlight' // Astro content routes, slug and draft frontmatter
|
|
134
|
+
| 'nextra' // content-directory routes and contentDirBasePath
|
|
133
135
|
| 'custom' // path to user plugin module
|
|
134
136
|
```
|
|
135
137
|
|
|
138
|
+
VitePress options accept `docsDir` (also `root` or `srcDir`), `urlPrefix`,
|
|
139
|
+
`cleanUrls`, and up to 64 declarative `srcExclude` glob patterns. Doc Bridge
|
|
140
|
+
uses `.html` routes unless `cleanUrls: true`, matching
|
|
141
|
+
VitePress routing without loading or executing `.vitepress/config.*`. Route
|
|
142
|
+
rewrites must therefore be reflected in the configured corpus or URL prefix.
|
|
143
|
+
|
|
144
|
+
Starlight options accept `contentDir` (also `docsDir` or `root`) and
|
|
145
|
+
`urlPrefix`. The adapter reads the standard `slug` and `draft` frontmatter,
|
|
146
|
+
follows the default filename sluggifier, excludes underscore-prefixed
|
|
147
|
+
partials, and never executes `astro.config.*`. Sites using a custom
|
|
148
|
+
`docsLoader({ generateId })` should add explicit `slug` frontmatter so Doc
|
|
149
|
+
Bridge can resolve the same public route without executing project code.
|
|
150
|
+
|
|
151
|
+
Nextra options accept `contentDir` (also `docsDir` or `root`), `urlPrefix`,
|
|
152
|
+
and `contentDirBasePath`. The adapter maps `index.md` and `index.mdx` to the
|
|
153
|
+
containing route and uses Doc Bridge `package`, `module`, or `id` frontmatter
|
|
154
|
+
as the join key. `urlPrefix` takes precedence over `contentDirBasePath`. Point
|
|
155
|
+
`contentDir` at either `content` or `src/content`; Doc Bridge never imports or
|
|
156
|
+
executes `next.config.*`, `_meta.*`, themes, or other project code. This
|
|
157
|
+
adapter targets Nextra's content-directory convention; app-router `page.mdx`
|
|
158
|
+
trees are not inferred by this plugin.
|
|
159
|
+
|
|
136
160
|
### Bridge to agent docs
|
|
137
161
|
|
|
138
162
|
When `corpus.human` is set, plugins MUST:
|
|
@@ -199,7 +223,7 @@ Without `routing`, handoffs are inferred from agent corpus links only. Monorepo
|
|
|
199
223
|
|
|
200
224
|
```ts
|
|
201
225
|
type RoutingConfig = {
|
|
202
|
-
plugin?: 'pnpm-monorepo' | 'npm-workspaces' | 'yarn-workspaces' | 'custom'
|
|
226
|
+
plugin?: 'pnpm-monorepo' | 'npm-workspaces' | 'yarn-workspaces' | 'nx' | 'pattern-files' | 'custom'
|
|
203
227
|
|
|
204
228
|
options?: {
|
|
205
229
|
/** Workspace globs; default from package manager */
|
|
@@ -240,6 +264,13 @@ type ChangeRouteEntry = {
|
|
|
240
264
|
}
|
|
241
265
|
```
|
|
242
266
|
|
|
267
|
+
`routing.plugin: 'nx'` reads `project.json` and package manifests with an `nx`
|
|
268
|
+
object. It does not load Nx plugins or execute workspace code. Project roots must
|
|
269
|
+
resolve inside the configured project root. Declared `test` targets become Nx test
|
|
270
|
+
checks; non-minimal gate presets also include declared `lint` targets. Explicit
|
|
271
|
+
`routing.options.ownership` remains authoritative. Targets created only at runtime
|
|
272
|
+
by Nx plugins are intentionally not inferred by this read-only adapter.
|
|
273
|
+
|
|
243
274
|
### Join keys (agent ↔ human ↔ ownership)
|
|
244
275
|
|
|
245
276
|
| Entity | Primary key | Human plugin maps via |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from '@agentskit/doc-bridge'
|
|
2
|
+
|
|
3
|
+
/** Nextra bridge only: no chat, no memory, no provider key. */
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
schemaVersion: 1,
|
|
6
|
+
corpus: {
|
|
7
|
+
agent: { root: 'docs/for-agents' },
|
|
8
|
+
human: {
|
|
9
|
+
plugin: 'nextra',
|
|
10
|
+
options: {
|
|
11
|
+
contentDir: 'content',
|
|
12
|
+
contentDirBasePath: '/docs',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
gates: { preset: 'standard' },
|
|
17
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineConfig } from '@agentskit/doc-bridge'
|
|
2
|
+
|
|
3
|
+
/** Nx — infer project ownership and available test/lint checks without executing Nx */
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
schemaVersion: 1,
|
|
6
|
+
corpus: {
|
|
7
|
+
agent: { root: 'docs/for-agents' },
|
|
8
|
+
},
|
|
9
|
+
routing: { plugin: 'nx' },
|
|
10
|
+
gates: { preset: 'standard' },
|
|
11
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from '@agentskit/doc-bridge'
|
|
2
|
+
|
|
3
|
+
/** Starlight bridge only: no chat, no memory, no provider key. */
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
schemaVersion: 1,
|
|
6
|
+
corpus: {
|
|
7
|
+
agent: { root: 'docs/for-agents' },
|
|
8
|
+
human: {
|
|
9
|
+
plugin: 'starlight',
|
|
10
|
+
options: {
|
|
11
|
+
contentDir: 'src/content/docs',
|
|
12
|
+
urlPrefix: '/docs',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
gates: { preset: 'standard' },
|
|
17
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from '@agentskit/doc-bridge'
|
|
2
|
+
|
|
3
|
+
/** VitePress bridge only: no chat, no memory, no provider key. */
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
schemaVersion: 1,
|
|
6
|
+
corpus: {
|
|
7
|
+
agent: { root: 'docs/for-agents' },
|
|
8
|
+
human: {
|
|
9
|
+
plugin: 'vitepress',
|
|
10
|
+
options: {
|
|
11
|
+
docsDir: 'docs',
|
|
12
|
+
urlPrefix: '/docs',
|
|
13
|
+
cleanUrls: true,
|
|
14
|
+
srcExclude: ['archive/**'],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
gates: { preset: 'standard' },
|
|
19
|
+
})
|
package/mcpb/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "doc-bridge",
|
|
4
4
|
"display_name": "Doc Bridge",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.3.0",
|
|
6
6
|
"description": "Deterministic repository handoffs for coding agents, running locally without an LLM or API key.",
|
|
7
7
|
"long_description": "Doc Bridge turns a repository's own documentation and ownership metadata into deterministic handoffs: where an agent should start, which paths it may edit, which checks it must run, and when a human must take over. The local connector exposes the same read-only contract available through Doc Bridge CLI and CI.",
|
|
8
8
|
"author": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentskit/doc-bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"mcpName": "io.github.AgentsKit-io/doc-bridge",
|
|
5
5
|
"description": "Human↔agent documentation bridge — deterministic handoffs, doc-site links, memory→docs, optional AgentsKit RAG/chat.",
|
|
6
6
|
"type": "module",
|
|
@@ -103,7 +103,9 @@
|
|
|
103
103
|
"access": "public"
|
|
104
104
|
},
|
|
105
105
|
"dependencies": {
|
|
106
|
-
"
|
|
106
|
+
"github-slugger": "^2.0.0",
|
|
107
|
+
"mermaid": "^11.16.1",
|
|
108
|
+
"minimatch": "^10.2.6",
|
|
107
109
|
"zod": "^3.24.2"
|
|
108
110
|
},
|
|
109
111
|
"peerDependencies": {
|
package/src/config/schema.ts
CHANGED
|
@@ -8,6 +8,8 @@ export const HumanCorpusPluginIdSchema = z.enum([
|
|
|
8
8
|
'docusaurus',
|
|
9
9
|
'mkdocs',
|
|
10
10
|
'vitepress',
|
|
11
|
+
'starlight',
|
|
12
|
+
'nextra',
|
|
11
13
|
'custom',
|
|
12
14
|
])
|
|
13
15
|
|
|
@@ -71,7 +73,7 @@ export const OwnershipEntrySchema = z
|
|
|
71
73
|
export const RoutingConfigSchema = z
|
|
72
74
|
.object({
|
|
73
75
|
plugin: z
|
|
74
|
-
.enum(['pnpm-monorepo', 'npm-workspaces', 'yarn-workspaces', 'pattern-files', 'custom'])
|
|
76
|
+
.enum(['pnpm-monorepo', 'npm-workspaces', 'yarn-workspaces', 'nx', 'pattern-files', 'custom'])
|
|
75
77
|
.optional(),
|
|
76
78
|
options: z
|
|
77
79
|
.object({
|
|
@@ -70,6 +70,7 @@ export const collectPackages = (
|
|
|
70
70
|
id: pkg.id,
|
|
71
71
|
path: existing.path || pkg.path,
|
|
72
72
|
...(pkg.name ? { name: pkg.name } : existing.name ? { name: existing.name } : {}),
|
|
73
|
+
...(pkg.checks ? { checks: pkg.checks } : existing.checks ? { checks: existing.checks } : {}),
|
|
73
74
|
})
|
|
74
75
|
}
|
|
75
76
|
|
|
@@ -163,6 +164,7 @@ export const buildLookup = (
|
|
|
163
164
|
const checks = [
|
|
164
165
|
...(override?.checks ??
|
|
165
166
|
fm?.checks ??
|
|
167
|
+
pkg.checks ??
|
|
166
168
|
defaultChecksForTarget(root, {
|
|
167
169
|
packageId: pkg.id,
|
|
168
170
|
packagePath: path,
|
|
@@ -9,6 +9,7 @@ import { renderCapabilitiesJson } from './capabilities.js'
|
|
|
9
9
|
import { sha256NormalizedV1 } from './content-hash.js'
|
|
10
10
|
import { renderLlmsTxt } from './llms-txt.js'
|
|
11
11
|
import { scanHumanDocs } from './human-adapters/index.js'
|
|
12
|
+
import { discoverNxProjects } from './plugins/nx.js'
|
|
12
13
|
import { discoverPnpmPackages } from './plugins/pnpm-monorepo.js'
|
|
13
14
|
import { scanAgentCorpus } from './scan-corpus.js'
|
|
14
15
|
|
|
@@ -66,7 +67,12 @@ export const buildDocBridgeIndex = (opts: BuildIndexOptions): BuildIndexResult =
|
|
|
66
67
|
config.routing?.plugin === 'npm-workspaces' ||
|
|
67
68
|
config.routing?.plugin === 'yarn-workspaces'
|
|
68
69
|
|
|
69
|
-
const discovered =
|
|
70
|
+
const discovered =
|
|
71
|
+
config.routing?.plugin === 'nx'
|
|
72
|
+
? discoverNxProjects(root, config)
|
|
73
|
+
: shouldDiscover
|
|
74
|
+
? discoverPnpmPackages(root, config)
|
|
75
|
+
: []
|
|
70
76
|
const packages = collectPackages(config, discovered, corpus)
|
|
71
77
|
const humanDocs = scanHumanDocs(root, config)
|
|
72
78
|
|
|
@@ -4,8 +4,11 @@ import { resolve, sep } from 'node:path'
|
|
|
4
4
|
import type { DocBridgeConfigV1, HumanCorpusConfig } from '../../config/schema.js'
|
|
5
5
|
import { docusaurusAdapter } from './docusaurus.js'
|
|
6
6
|
import { fumadocsAdapter } from './fumadocs.js'
|
|
7
|
+
import { nextraAdapter } from './nextra.js'
|
|
7
8
|
import type { HumanAdapter, HumanDocMap, HumanDocRecord } from './core.js'
|
|
8
9
|
import { plainMarkdownAdapter } from './plain-markdown.js'
|
|
10
|
+
import { starlightAdapter } from './starlight.js'
|
|
11
|
+
import { vitepressAdapter } from './vitepress.js'
|
|
9
12
|
|
|
10
13
|
export type { HumanAdapter, HumanDocMap, HumanDocRecord } from './core.js'
|
|
11
14
|
|
|
@@ -13,6 +16,9 @@ const ADAPTERS: readonly HumanAdapter[] = [
|
|
|
13
16
|
plainMarkdownAdapter,
|
|
14
17
|
fumadocsAdapter,
|
|
15
18
|
docusaurusAdapter,
|
|
19
|
+
vitepressAdapter,
|
|
20
|
+
starlightAdapter,
|
|
21
|
+
nextraAdapter,
|
|
16
22
|
]
|
|
17
23
|
|
|
18
24
|
const humanConfigs = (config: DocBridgeConfigV1): HumanCorpusConfig[] => {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
optionString,
|
|
3
|
+
parseFrontmatter,
|
|
4
|
+
routeSlug,
|
|
5
|
+
scanMarkdownDocs,
|
|
6
|
+
type HumanAdapter,
|
|
7
|
+
type HumanDocRecord,
|
|
8
|
+
} from './core.js'
|
|
9
|
+
|
|
10
|
+
const nextraRecordId = (relPath: string, raw: string): string => {
|
|
11
|
+
const frontmatter = parseFrontmatter(raw)
|
|
12
|
+
return frontmatter.package ?? frontmatter.module ?? frontmatter.id ?? (routeSlug(relPath) || 'index')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const isIndexPage = (path: string): boolean => /(^|[/\\])index\.mdx?$/i.test(path)
|
|
16
|
+
|
|
17
|
+
const resolveRouteCollisions = (records: readonly HumanDocRecord[]): HumanDocRecord[] => {
|
|
18
|
+
const byUrl = new Map<string, { readonly record: HumanDocRecord; readonly indexPage: boolean }>()
|
|
19
|
+
for (const record of records) {
|
|
20
|
+
const normalized = { ...record, url: record.url || '/' }
|
|
21
|
+
const indexPage = isIndexPage(record.path)
|
|
22
|
+
const existing = byUrl.get(normalized.url)
|
|
23
|
+
if (!existing || indexPage || !existing.indexPage) {
|
|
24
|
+
byUrl.set(normalized.url, { record: normalized, indexPage })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...byUrl.values()].map(({ record }) => record)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const nextraAdapter: HumanAdapter = {
|
|
31
|
+
plugin: 'nextra',
|
|
32
|
+
scan: ({ root, config }) => {
|
|
33
|
+
const contentDir = optionString(config.options, ['contentDir', 'docsDir', 'root'])
|
|
34
|
+
if (!contentDir) return []
|
|
35
|
+
|
|
36
|
+
return resolveRouteCollisions(
|
|
37
|
+
scanMarkdownDocs(root, contentDir, {
|
|
38
|
+
idForDoc: nextraRecordId,
|
|
39
|
+
urlPrefix: optionString(config.options, ['urlPrefix', 'contentDirBasePath']) ?? '/',
|
|
40
|
+
}),
|
|
41
|
+
)
|
|
42
|
+
},
|
|
43
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { slug as githubSlug } from 'github-slugger'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
optionString,
|
|
5
|
+
parseFrontmatter,
|
|
6
|
+
routeSlug,
|
|
7
|
+
scanMarkdownDocs,
|
|
8
|
+
type HumanAdapter,
|
|
9
|
+
} from './core.js'
|
|
10
|
+
|
|
11
|
+
const isStarlightPage = (relPath: string, raw: string): boolean => {
|
|
12
|
+
if (relPath.split('/').some((part) => part.startsWith('.') || part.startsWith('_'))) return false
|
|
13
|
+
return parseFrontmatter(raw).draft !== 'true'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const starlightFileSlug = (relPath: string): string =>
|
|
17
|
+
routeSlug(relPath)
|
|
18
|
+
.split('/')
|
|
19
|
+
.map((part) => githubSlug(part))
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.join('/')
|
|
22
|
+
|
|
23
|
+
const starlightSlug = (relPath: string, raw: string): string => {
|
|
24
|
+
const slug = parseFrontmatter(raw).slug
|
|
25
|
+
return slug ? slug.replace(/^\/+|\/+$/g, '') : starlightFileSlug(relPath)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const starlightAdapter: HumanAdapter = {
|
|
29
|
+
plugin: 'starlight',
|
|
30
|
+
scan: ({ root, config }) => {
|
|
31
|
+
const contentDir = optionString(config.options, ['contentDir', 'docsDir', 'root'])
|
|
32
|
+
if (!contentDir) return []
|
|
33
|
+
|
|
34
|
+
return scanMarkdownDocs(root, contentDir, {
|
|
35
|
+
includeRelPath: isStarlightPage,
|
|
36
|
+
slugForDoc: starlightSlug,
|
|
37
|
+
urlPrefix: config.options?.urlPrefix,
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { minimatch } from 'minimatch'
|
|
2
|
+
|
|
3
|
+
import { optionString, routeSlug, scanMarkdownDocs, type HumanAdapter } from './core.js'
|
|
4
|
+
|
|
5
|
+
const isVitePressPage = (relPath: string): boolean =>
|
|
6
|
+
!relPath.split('/').some((part) => part === '.vitepress' || part.startsWith('.'))
|
|
7
|
+
|
|
8
|
+
const srcExcludePatterns = (value: unknown): readonly string[] => {
|
|
9
|
+
if (value === undefined) return []
|
|
10
|
+
if (!Array.isArray(value) || value.length > 64) {
|
|
11
|
+
throw new Error('VitePress srcExclude must be an array of at most 64 glob patterns.')
|
|
12
|
+
}
|
|
13
|
+
return value.map((pattern) => {
|
|
14
|
+
if (typeof pattern !== 'string' || !pattern || pattern.length > 256 || pattern.includes('\0')) {
|
|
15
|
+
throw new Error('Each VitePress srcExclude pattern must be a non-empty string up to 256 characters.')
|
|
16
|
+
}
|
|
17
|
+
return pattern
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const isExcluded = (relPath: string, patterns: readonly string[]): boolean =>
|
|
22
|
+
patterns.some((pattern) => minimatch(relPath, pattern, { dot: true }))
|
|
23
|
+
|
|
24
|
+
const vitepressSlug = (relPath: string, cleanUrls: boolean): string => {
|
|
25
|
+
const slug = routeSlug(relPath)
|
|
26
|
+
if (cleanUrls || /(?:^|\/)index\.mdx?$/.test(relPath)) return slug
|
|
27
|
+
return `${slug}.html`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const vitepressAdapter: HumanAdapter = {
|
|
31
|
+
plugin: 'vitepress',
|
|
32
|
+
scan: ({ root, config }) => {
|
|
33
|
+
const docsDir = optionString(config.options, ['docsDir', 'root', 'srcDir'])
|
|
34
|
+
if (!docsDir) return []
|
|
35
|
+
const srcExclude = srcExcludePatterns(config.options?.srcExclude)
|
|
36
|
+
|
|
37
|
+
return scanMarkdownDocs(root, docsDir, {
|
|
38
|
+
includeRelPath: (relPath) => isVitePressPage(relPath) && !isExcluded(relPath, srcExclude),
|
|
39
|
+
slugForDoc: (relPath) => vitepressSlug(relPath, config.options?.cleanUrls === true),
|
|
40
|
+
urlPrefix: config.options?.urlPrefix,
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs'
|
|
2
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { DocBridgeConfigV1 } from '../../config/schema.js'
|
|
5
|
+
import { detectPackageManager } from '../../lib/package-manager.js'
|
|
6
|
+
import { toPosix } from '../../lib/paths.js'
|
|
7
|
+
import { walkFiles } from '../../lib/walk.js'
|
|
8
|
+
import type { DiscoveredPackage } from './pnpm-monorepo.js'
|
|
9
|
+
|
|
10
|
+
const NX_SCAN_SKIP = new Set([
|
|
11
|
+
'node_modules',
|
|
12
|
+
'.git',
|
|
13
|
+
'.nx',
|
|
14
|
+
'dist',
|
|
15
|
+
'coverage',
|
|
16
|
+
'.doc-bridge',
|
|
17
|
+
])
|
|
18
|
+
const NX_PROJECT_NAME = /^(?:@[A-Za-z0-9._-]+\/)?[A-Za-z0-9][A-Za-z0-9._-]*$/
|
|
19
|
+
|
|
20
|
+
type JsonRecord = Record<string, unknown>
|
|
21
|
+
|
|
22
|
+
const isRecord = (value: unknown): value is JsonRecord =>
|
|
23
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
24
|
+
|
|
25
|
+
const readJsonRecord = (path: string): JsonRecord | undefined => {
|
|
26
|
+
try {
|
|
27
|
+
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
|
|
28
|
+
return isRecord(parsed) ? parsed : undefined
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const safeProjectPath = (
|
|
35
|
+
root: string,
|
|
36
|
+
manifestDir: string,
|
|
37
|
+
declaredRoot?: unknown,
|
|
38
|
+
): string | undefined => {
|
|
39
|
+
const candidate = typeof declaredRoot === 'string' ? resolve(root, declaredRoot) : manifestDir
|
|
40
|
+
const rel = toPosix(relative(root, candidate)) || '.'
|
|
41
|
+
if (isAbsolute(rel) || rel === '..' || rel.startsWith('../')) return undefined
|
|
42
|
+
try {
|
|
43
|
+
if (!lstatSync(candidate).isDirectory()) return undefined
|
|
44
|
+
const canonicalRoot = realpathSync.native(root)
|
|
45
|
+
const canonicalCandidate = realpathSync.native(candidate)
|
|
46
|
+
const canonicalRel = relative(canonicalRoot, canonicalCandidate)
|
|
47
|
+
if (
|
|
48
|
+
isAbsolute(canonicalRel) ||
|
|
49
|
+
canonicalRel === '..' ||
|
|
50
|
+
canonicalRel.startsWith(`..${sep}`)
|
|
51
|
+
) {
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
return rel
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const packageId = (name: string): string =>
|
|
61
|
+
name.startsWith('@') ? (name.split('/').at(-1) ?? name) : name
|
|
62
|
+
|
|
63
|
+
const commandPrefix = (root: string): string => {
|
|
64
|
+
const manager = detectPackageManager(root)
|
|
65
|
+
if (manager === 'pnpm') return 'pnpm exec nx run'
|
|
66
|
+
if (manager === 'yarn') return 'yarn nx run'
|
|
67
|
+
if (manager === 'bun') return 'bunx nx run'
|
|
68
|
+
return 'npx nx run'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const inferredChecks = (
|
|
72
|
+
root: string,
|
|
73
|
+
config: DocBridgeConfigV1,
|
|
74
|
+
projectName: string,
|
|
75
|
+
targets: ReadonlySet<string>,
|
|
76
|
+
): string[] | undefined => {
|
|
77
|
+
const prefix = commandPrefix(root)
|
|
78
|
+
const strict = (config.gates?.preset ?? 'minimal') !== 'minimal'
|
|
79
|
+
const checks = [
|
|
80
|
+
...(targets.has('test') ? [`${prefix} ${projectName}:test`] : []),
|
|
81
|
+
...(strict && targets.has('lint') ? [`${prefix} ${projectName}:lint`] : []),
|
|
82
|
+
]
|
|
83
|
+
return checks.length ? checks : undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type RankedProject = DiscoveredPackage & {
|
|
87
|
+
readonly rank: number
|
|
88
|
+
readonly targets: readonly string[]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const discoverNxProjects = (
|
|
92
|
+
root: string,
|
|
93
|
+
config: DocBridgeConfigV1,
|
|
94
|
+
): DiscoveredPackage[] => {
|
|
95
|
+
if (!existsSync(resolve(root, 'nx.json'))) return []
|
|
96
|
+
|
|
97
|
+
const manifests = walkFiles(root, {
|
|
98
|
+
extensions: ['project.json', 'package.json'],
|
|
99
|
+
skipDirs: NX_SCAN_SKIP,
|
|
100
|
+
maxFiles: 10_000,
|
|
101
|
+
})
|
|
102
|
+
const projects = new Map<string, RankedProject>()
|
|
103
|
+
|
|
104
|
+
for (const manifest of manifests) {
|
|
105
|
+
const json = readJsonRecord(manifest)
|
|
106
|
+
if (!json) continue
|
|
107
|
+
const manifestDir = dirname(manifest)
|
|
108
|
+
const manifestName = basename(manifest)
|
|
109
|
+
const isProjectJson = manifestName === 'project.json'
|
|
110
|
+
if (!isProjectJson && manifestName !== 'package.json') continue
|
|
111
|
+
const nx = json.nx
|
|
112
|
+
if (!isProjectJson && !isRecord(nx)) continue
|
|
113
|
+
|
|
114
|
+
const path = safeProjectPath(root, manifestDir, isProjectJson ? json.root : undefined)
|
|
115
|
+
if (!path) continue
|
|
116
|
+
const projectPackage = isProjectJson
|
|
117
|
+
? readJsonRecord(resolve(root, path, 'package.json'))
|
|
118
|
+
: undefined
|
|
119
|
+
const projectName =
|
|
120
|
+
typeof json.name === 'string'
|
|
121
|
+
? json.name
|
|
122
|
+
: typeof projectPackage?.name === 'string'
|
|
123
|
+
? projectPackage.name
|
|
124
|
+
: basename(path === '.' ? root : path)
|
|
125
|
+
if (!NX_PROJECT_NAME.test(projectName)) continue
|
|
126
|
+
|
|
127
|
+
const targets = new Set<string>()
|
|
128
|
+
const targetRecord = isProjectJson ? json.targets : isRecord(nx) ? nx.targets : undefined
|
|
129
|
+
if (isRecord(targetRecord)) {
|
|
130
|
+
for (const target of Object.keys(targetRecord)) targets.add(target)
|
|
131
|
+
}
|
|
132
|
+
if (!isProjectJson && isRecord(json.scripts)) {
|
|
133
|
+
for (const script of Object.keys(json.scripts)) targets.add(script)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const id = packageId(projectName)
|
|
137
|
+
const rank = isProjectJson ? 2 : 1
|
|
138
|
+
const existing = projects.get(id)
|
|
139
|
+
if (existing && existing.path !== path) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Nx project identity collision for "${id}": "${existing.name ?? id}" at "${existing.path}" and "${projectName}" at "${path}". Use unique Nx project names.`,
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
const mergedTargets = [...new Set([...(existing?.targets ?? []), ...targets])]
|
|
145
|
+
const projectJsonWins = rank >= (existing?.rank ?? 0)
|
|
146
|
+
const resolvedName = projectJsonWins ? projectName : (existing?.name ?? projectName)
|
|
147
|
+
const checks = inferredChecks(root, config, resolvedName, new Set(mergedTargets))
|
|
148
|
+
projects.set(id, {
|
|
149
|
+
id,
|
|
150
|
+
path,
|
|
151
|
+
name: resolvedName,
|
|
152
|
+
...(checks ? { checks } : {}),
|
|
153
|
+
rank: projectJsonWins ? rank : (existing?.rank ?? rank),
|
|
154
|
+
targets: mergedTargets,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return [...projects.values()]
|
|
159
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
160
|
+
.map(({ rank: _rank, targets: _targets, ...project }) => project)
|
|
161
|
+
}
|
|
@@ -13,6 +13,7 @@ export type WatchIndexOptions = {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const WATCH_PATTERN = /\.(md|mdx|json|ya?ml|mdc)$/i
|
|
16
|
+
const NX_MANIFEST_PATTERN = /(^|[/\\])(project|package)\.json$/i
|
|
16
17
|
|
|
17
18
|
const collectWatchRoots = (root: string, config: DocBridgeConfigV1, configPath?: string): string[] => {
|
|
18
19
|
const roots = new Set<string>()
|
|
@@ -24,7 +25,7 @@ const collectWatchRoots = (root: string, config: DocBridgeConfigV1, configPath?:
|
|
|
24
25
|
: []
|
|
25
26
|
for (const source of humanSources) {
|
|
26
27
|
const humanOpts = source.options ?? {}
|
|
27
|
-
for (const key of ['contentDir', 'docsDir', 'root']) {
|
|
28
|
+
for (const key of ['contentDir', 'docsDir', 'root', 'srcDir']) {
|
|
28
29
|
const value = humanOpts[key]
|
|
29
30
|
if (typeof value === 'string' && value.length) roots.add(resolve(root, value))
|
|
30
31
|
}
|
|
@@ -74,6 +75,14 @@ export const watchDocBridgeIndex = (opts: WatchIndexOptions): Promise<number> =>
|
|
|
74
75
|
})
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
const nxRoot = resolve(opts.root)
|
|
79
|
+
if (opts.config.routing?.plugin === 'nx' && existsSync(nxRoot)) {
|
|
80
|
+
watch(nxRoot, { recursive: true }, (_event, filename) => {
|
|
81
|
+
if (!filename || !NX_MANIFEST_PATTERN.test(filename)) return
|
|
82
|
+
rebuild()
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
const configDir = resolve(opts.root)
|
|
78
87
|
if (existsSync(configDir)) {
|
|
79
88
|
watch(configDir, (_event, filename) => {
|
|
@@ -91,4 +100,4 @@ export const watchDocBridgeIndex = (opts: WatchIndexOptions): Promise<number> =>
|
|
|
91
100
|
process.once('SIGINT', onSignal)
|
|
92
101
|
process.once('SIGTERM', onSignal)
|
|
93
102
|
})
|
|
94
|
-
}
|
|
103
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -155,6 +155,7 @@ export {
|
|
|
155
155
|
export { PACKAGE_VERSION } from './version.js'
|
|
156
156
|
|
|
157
157
|
export { collectPackages, buildLookup } from './index-builder/build-handoffs.js'
|
|
158
|
+
export { discoverNxProjects } from './index-builder/plugins/nx.js'
|
|
158
159
|
export { projectRootFromConfigPath } from './config/load-config.js'
|
|
159
160
|
export { createDocBridgeRag } from './intelligence/rag.js'
|
|
160
161
|
export { runChatOnce, startInkChat } from './intelligence/chat.js'
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '1.
|
|
1
|
+
export const PACKAGE_VERSION = '1.3.0'
|