@devinnn/docdrift 0.1.7 → 0.1.12

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/README.md CHANGED
@@ -2,198 +2,98 @@
2
2
 
3
3
  Docs that never lie: detect drift between merged code and docs, then open low-noise, evidence-grounded remediation via Devin sessions.
4
4
 
5
+ ## Table of contents
6
+
7
+ - [Deliverables](#deliverables)
8
+ - [Quick start](#quick-start)
9
+ - [Modes & spec providers](#modes--spec-providers)
10
+ - [Guides](#guides)
11
+ - [Project docs layout](#project-docs-layout)
12
+
13
+ ---
14
+
5
15
  ## Deliverables
6
16
 
7
17
  - **npm package**: [@devinnn/docdrift](https://www.npmjs.com/package/@devinnn/docdrift) — TypeScript CLI (`docdrift`)
8
- - `validate`
9
- - `detect --base <sha> --head <sha>`
10
- - `run --base <sha> --head <sha>`
11
- - `status --since 24h`
18
+ - `validate` — Validate docdrift.yaml
19
+ - `detect --base <sha> --head <sha>` — Check for drift
20
+ - `run --base <sha> --head <sha>` — Full run with Devin
21
+ - `status --since 24h` — Show run status
12
22
  - `sla-check` — Check for doc-drift PRs open 7+ days and open a reminder issue
13
- - GitHub Action: `/Users/cameronking/Desktop/sideproject/docdrift/.github/workflows/devin-doc-drift.yml`
14
- - Repo-local config: `/Users/cameronking/Desktop/sideproject/docdrift/docdrift.yaml`
23
+ - `setup` Interactive setup (Devin analyzes repo, generates v2 docdrift.yaml)
24
+ - `generate-yaml` — Generate config from repo fingerprint `[--output path] [--force]`
25
+ - GitHub Action: `.github/workflows/devin-doc-drift.yml`
26
+ - Repo-local config: `docdrift.yaml`
15
27
  - Demo API + OpenAPI exporter + driftable docs
16
- - PR template + Loom script
17
-
18
- ## Why this is low-noise
19
-
20
- - **Single session, single PR** — One Devin session handles the whole docsite (API reference + guides).
21
- - **Gate on API spec diff** — We only run when OpenAPI drift is detected; no session for docs-check-only failures.
22
- - **requireHumanReview** — When the PR touches guides/prose, we open an issue after the PR to direct attention.
23
- - **7-day SLA** — If a doc-drift PR is open 7+ days, we open a reminder issue (configurable `slaDays`; use `sla-check` CLI or cron workflow).
24
- - Confidence gating and allowlist/exclude enforcement.
25
- - Idempotency key prevents duplicate actions for same repo/SHAs/action.
26
-
27
- ## Detection and gate
28
-
29
- - **Gate:** We only run a Devin session when **OpenAPI drift** is detected. No drift → no session.
30
- - Tier 1: OpenAPI drift (`openapi/generated.json` vs published spec)
31
- - Tier 2: Heuristic path impacts from docAreas (e.g. `apps/api/src/auth/**` → guides)
32
-
33
- Output artifacts (under `.docdrift/`):
34
-
35
- - `drift_report.json`
36
- - `metrics.json` (after `run`)
28
+ - PR template + [Loom script](loom.md)
37
29
 
38
- When you run docdrift as a package (e.g. `npx docdrift` or from another repo), all of this is written to **that repo’s** `.docdrift/` — i.e. the current working directory where the CLI is invoked, not inside the package. Add `.docdrift/` to the consuming repo’s `.gitignore` if you don’t want to commit run artifacts.
30
+ ---
39
31
 
40
- ## Core flow (`docdrift run`)
41
-
42
- 1. Validate config and command availability.
43
- 2. Build drift report. **Gate:** If no OpenAPI drift, exit (no session).
44
- 3. Policy decision (`OPEN_PR | UPDATE_EXISTING_PR | OPEN_ISSUE | NOOP`).
45
- 4. Build one aggregated evidence bundle for the whole docsite.
46
- 5. One Devin session with whole-docsite prompt; poll to terminal status.
47
- 6. If PR opened and touches `requireHumanReview` paths → create issue to direct attention.
48
- 7. Surface result via GitHub commit comment; open issue on blocked/low-confidence paths.
49
- 8. Persist state (including `lastDocDriftPrUrl` for SLA); write `.docdrift/metrics.json`.
50
-
51
- ## Where the docs are (this repo)
52
-
53
- | Path | Purpose |
54
- | ------------------------------------------ | ----------------------------------------------------------------------- |
55
- | `apps/docs-site/openapi/openapi.json` | Published OpenAPI spec (docdrift updates this when drift is detected). |
56
- | `apps/docs-site/docs/api/` | API reference MDX generated from the spec (`npm run docs:gen`). |
57
- | `apps/docs-site/docs/guides/auth.md` | Conceptual auth guide (updated only for conceptual drift). |
58
-
59
- The docsite is a Docusaurus app with `docusaurus-plugin-openapi-docs`. The **generated** spec from code lives at `openapi/generated.json` (from `npm run openapi:export`). Drift = generated vs published differ. Verification runs `docs:gen` and `docs:build` so the docsite actually builds.
60
-
61
- ## How Devin updates them
62
-
63
- 1. **Evidence bundle** — Docdrift builds a tarball with the drift report, OpenAPI diff, and impacted doc snippets, and uploads it to the Devin API as session attachments.
64
- 2. **Devin session** — Devin is prompted (see `src/devin/prompts.ts`) to update only files under the allowlist (`openapi/**`, `apps/docs-site/**`), make minimal correct edits, run verification (`npm run docs:gen`, `npm run docs:build`), and open **one PR** per doc area with a clear description.
65
- 3. **PR** — Devin updates `apps/docs-site/openapi/openapi.json` to match the current API, runs `docs:gen` to regenerate API reference MDX, and opens a pull request. You review and merge; the docsite builds and the docs are updated.
66
-
67
- So the “fix” is a **PR opened by Devin** that you merge; the repo’s docs don’t change until that PR is merged.
68
-
69
- ## Local usage
32
+ ## Quick start
70
33
 
71
34
  ```bash
72
- npm install
73
- npx tsx src/cli.ts validate
74
- npm run openapi:export
75
- npx tsx src/cli.ts detect --base <sha> --head <sha>
76
- DEVIN_API_KEY=... GITHUB_TOKEN=... GITHUB_REPOSITORY=owner/repo GITHUB_SHA=<sha> npx tsx src/cli.ts run --base <sha> --head <sha>
35
+ # Interactive setup (requires DEVIN_API_KEY; add repo in Devin Machine first)
36
+ npx @devinnn/docdrift setup
37
+
38
+ # Or generate config only (scriptable)
39
+ npx @devinnn/docdrift generate-yaml --output docdrift.yaml --force
77
40
  ```
78
41
 
79
- ## Local demo (no GitHub)
80
-
81
- You can run a full end-to-end demo locally with no remote repo. Ensure `.env` has `DEVIN_API_KEY` (and optionally `GITHUB_TOKEN` only when you have a real repo).
82
-
83
- 1. **One-time setup (already done if you have two commits with drift)**
84
- - Git is inited; baseline commit has docs in sync with API.
85
- - A later commit changes `apps/api/src/model.ts` (e.g. `name` → `fullName`) and runs `npm run openapi:export`, so `openapi/generated.json` drifts from `docs/reference/openapi.json`.
86
-
87
- 2. **Run the pipeline**
88
-
89
- ```bash
90
- npm install
91
- npx tsx src/cli.ts validate
92
- npx tsx src/cli.ts detect --base b0f624f --head 6030902
93
- ```
94
-
95
- - Use your own `git log --oneline -3` to get `base` (older) and `head` (newer) SHAs if you recreated the demo.
42
+ [**Setup guide**](docs/guides/setup.md) — Setup options, prerequisites
96
43
 
97
- 3. **Run with Devin (no GitHub calls)**
98
- Omit `GITHUB_TOKEN` so the CLI does not post comments or create issues. Devin session still runs; results are printed to stdout and written to `.docdrift/state.json` and `metrics.json`.
44
+ ---
99
45
 
100
- ```bash
101
- export $(grep -v '^#' .env | xargs)
102
- unset GITHUB_TOKEN GITHUB_REPOSITORY GITHUB_SHA
103
- npx tsx src/cli.ts run --base b0f624f --head 6030902
104
- ```
46
+ ## Modes & spec providers
105
47
 
106
- - `run` can take 1–3 minutes while the Devin session runs.
48
+ | Mode | When it runs |
49
+ | ---- | -------------- |
50
+ | **strict** (default) | Only when spec drift is detected (OpenAPI, GraphQL, etc.). No spec drift → no Devin session. |
51
+ | **auto** | Also when pathMappings match (file changes hit `match` patterns). |
107
52
 
108
- 4. **What you’ll see**
109
- - `.docdrift/drift_report.json` — drift items (e.g. OpenAPI `name` → `fullName`).
110
- - `.docdrift/evidence/<runId>/` — evidence bundles and OpenAPI diff.
111
- - Stdout — per–doc-area outcome (e.g. PR opened by Devin or blocked).
112
- - `.docdrift/metrics.json` — counts and timing.
53
+ | Spec formats | openapi3, swagger2, graphql, fern, postman |
113
54
 
114
- ## CI usage
55
+ [**Configuration**](docs/guides/configuration.md) — Modes, spec providers, full config
115
56
 
116
- - Add secret: `DEVIN_API_KEY`
117
- - Push to `main` or run `workflow_dispatch`
118
- - Action uploads:
119
- - `.docdrift/drift_report.json`
120
- - `.docdrift/evidence/**`
121
- - `.docdrift/metrics.json`
57
+ ---
122
58
 
123
- ## Run on GitHub
59
+ ## Guides
124
60
 
125
- 1. **Create a repo** on GitHub (e.g. `your-org/docdrift`), then add the remote and push:
61
+ | Guide | What’s inside |
62
+ | ----- | -------------- |
63
+ | [Setup](docs/guides/setup.md) | `setup` vs `generate-yaml`, prerequisites |
64
+ | [Configuration](docs/guides/configuration.md) | Modes, spec providers; links to full schema |
65
+ | [How it works](docs/guides/how-it-works.md) | Detection, gate, core flow, low-noise design |
66
+ | [Ecosystems](docs/guides/ecosystems.md) | OpenAPI, FastAPI, Fern, GraphQL, Mintlify, Postman, monorepos |
67
+ | [Local development](docs/guides/local-development.md) | Local usage, demo without GitHub |
68
+ | [CI & GitHub](docs/guides/ci-github.md) | GitHub Actions, secrets, demo on GitHub |
69
+ | [Using in another repo](docs/guides/consuming-repo.md) | Published package, CLI, GitHub Actions |
70
+ | [Publishing](docs/guides/publishing.md) | Publishing the npm package |
71
+ | [Loom script](loom.md) | Recording script for demos |
126
72
 
127
- ```bash
128
- git remote add origin https://github.com/your-org/docdrift.git
129
- git push -u origin main
130
- ```
73
+ ### Reference
131
74
 
132
- 2. **Add secret**
133
- Repo → **Settings** → **Secrets and variables** → **Actions** → **New repository secret**
134
- - Name: `DEVIN_API_KEY`
135
- - Value: your Devin API key (same as in `.env` locally)
75
+ - [docdrift.yaml](docdrift-yml.md) Full configuration schema and validation
136
76
 
137
- `GITHUB_TOKEN` is provided automatically; the workflow uses it for commit comments and issues.
77
+ ---
138
78
 
139
- 3. **Trigger the workflow**
140
- - **Push to `main`** — runs on every push (compares previous commit vs current).
141
- - **Manual run** — **Actions** tab → **devin-doc-drift** → **Run workflow** (uses `HEAD` and `HEAD^` as head/base).
79
+ ## Project docs layout (this repo)
142
80
 
143
- ## See it work (demo on GitHub)
81
+ | Path | Purpose |
82
+ | ---- | ------- |
83
+ | `apps/docs-site/openapi/openapi.json` | Published OpenAPI spec (docdrift updates when drift detected) |
84
+ | `apps/docs-site/docs/api/` | API reference MDX (`npm run docs:gen`) |
85
+ | `apps/docs-site/docs/guides/` | Conceptual guides (auth, etc.) |
144
86
 
145
- This repo has **intentional drift**: the API has been expanded (new fields `fullName`, `avatarUrl`, `createdAt`, `role` and new endpoint `GET /v1/users` with pagination), but **docs are unchanged** (`docs/reference/openapi.json` and `docs/reference/api.md` still describe the old single-endpoint, `id`/`name`/`email` only). Running docdrift will detect that and hand a large diff to Devin to fix via a PR. To see it:
87
+ Generated spec from code: `openapi/generated.json` (`npm run openapi:export`). Drift = generated vs published differ.
146
88
 
147
- 1. **Create a new GitHub repo** (e.g. `docdrift-demo`) so you have a clean place to run the workflow.
148
- 2. **Push this project with full history** (so both commits are on `main`):
149
- ```bash
150
- git remote add origin https://github.com/YOUR_ORG/docdrift-demo.git
151
- git push -u origin main
152
- ```
153
- 3. **Add secret** in that repo: **Settings** → **Secrets and variables** → **Actions** → `DEVIN_API_KEY` = your Devin API key.
154
- 4. **Trigger the workflow**
155
- - Either push another small commit (e.g. README tweak), or
156
- - **Actions** → **devin-doc-drift** → **Run workflow**.
157
- 5. **Where to look**
158
- - **Actions** → open the run → **Run Doc Drift** step: the step logs print JSON with `sessionUrl`, `prUrl`, and `outcome` per doc area. Open any `sessionUrl` in your browser to see the Devin session.
159
- - **Artifacts**: download **docdrift-artifacts** for `.docdrift/drift_report.json`, `.docdrift/metrics.json`, and evidence.
160
- - **Devin dashboard**: sessions are tagged `docdrift`; you’ll see the run there once the step completes (often 1–3 minutes).
89
+ ---
161
90
 
162
- ## Using in another repo (published package)
91
+ ## Why low-noise
163
92
 
164
- Once published to npm, any repo can use the CLI locally or in GitHub Actions.
93
+ - **Single session, single PR** One Devin session for the whole docsite
94
+ - **Gate on spec diff** — No session when no drift (strict mode)
95
+ - **requireHumanReview** — Issue when PR touches guides/prose
96
+ - **7-day SLA** — Reminder issue for stale doc-drift PRs
97
+ - **Confidence gating** — Allowlist, exclude, idempotency
165
98
 
166
- 1. **Setup** `npx @devinnn/docdrift setup` (requires `DEVIN_API_KEY`). Devin generates `docdrift.yaml`, `.docdrift/DocDrift.md`, and `.github/workflows/docdrift.yml`. Prerequisite: add your repo in Devin's Machine first. Or add `docdrift.yaml` manually (see `docdrift-yml.md`).
167
- 2. **CLI**
168
- ```bash
169
- npx @devinnn/docdrift validate
170
- npx @devinnn/docdrift detect --base <base-sha> --head <head-sha>
171
- # With env for run:
172
- DEVIN_API_KEY=... GITHUB_TOKEN=... GITHUB_REPOSITORY=owner/repo GITHUB_SHA=<sha> npx @devinnn/docdrift run --base <base-sha> --head <head-sha>
173
- ```
174
- 3. **GitHub Actions** — add a step that runs the CLI (e.g. after checkout and setting base/head):
175
- ```yaml
176
- - run: npx @devinnn/docdrift run --base ${{ steps.shas.outputs.base }} --head ${{ steps.shas.outputs.head }}
177
- env:
178
- DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY }}
179
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
180
- GITHUB_REPOSITORY: ${{ github.repository }}
181
- GITHUB_SHA: ${{ github.sha }}
182
- ```
183
- Add repo secret `DEVIN_API_KEY`; `GITHUB_TOKEN` is provided by the runner.
184
-
185
- ## Publishing the package
186
-
187
- - Set `"private": false` in `package.json` (or omit it).
188
- - Set `"repository": { "type": "git", "url": "https://github.com/your-org/docdrift.git" }`.
189
- - Run `pnpm build` (or `npm run build`), then `npm publish` (for a scoped package use `npm publish --access public`).
190
- - Only the `dist/` directory is included (`files` in `package.json`). Consumers get the built CLI; they provide their own `docdrift.yaml` in their repo.
191
-
192
- ## Demo scenario
193
-
194
- - Autogen drift: rename a field in `apps/api/src/model.ts`, merge to `main`, observe docs PR path.
195
- - Conceptual drift: change auth behavior under `apps/api/src/auth/**`, merge to `main`, observe single escalation issue.
196
-
197
- ## Loom
198
-
199
- See `/Users/cameronking/Desktop/sideproject/docdrift/loom.md` for the minute-by-minute recording script.
99
+ [**How it works**](docs/guides/how-it-works.md) Detection, flow, evidence bundle
@@ -112,8 +112,10 @@ function hasPrUrl(session) {
112
112
  return true;
113
113
  return false;
114
114
  }
115
+ const PROGRESS_INTERVAL_MS = 30_000; // Print "still waiting" every 30s
115
116
  async function pollUntilTerminal(apiKey, sessionId, timeoutMs = 30 * 60_000) {
116
117
  const started = Date.now();
118
+ let lastProgressAt = 0;
117
119
  while (Date.now() - started < timeoutMs) {
118
120
  const session = await devinGetSession(apiKey, sessionId);
119
121
  const status = String(session.status_enum ?? session.status ?? "UNKNOWN").toLowerCase();
@@ -124,6 +126,12 @@ async function pollUntilTerminal(apiKey, sessionId, timeoutMs = 30 * 60_000) {
124
126
  if (hasPrUrl(session)) {
125
127
  return session;
126
128
  }
129
+ const now = Date.now();
130
+ if (now - lastProgressAt >= PROGRESS_INTERVAL_MS) {
131
+ const elapsed = Math.round((now - started) / 1000);
132
+ process.stdout.write(` Still waiting for Devin… (${elapsed}s elapsed; open session URL in browser to watch)\n`);
133
+ lastProgressAt = now;
134
+ }
127
135
  await new Promise((resolve) => setTimeout(resolve, 5000));
128
136
  }
129
137
  throw new Error(`Session polling timed out for ${sessionId}`);
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.heuristicInference = heuristicInference;
6
7
  exports.inferConfigFromFingerprint = inferConfigFromFingerprint;
7
8
  const gateway_1 = require("@ai-sdk/gateway");
8
9
  const ai_1 = require("ai");
@@ -100,21 +101,32 @@ function writeCache(cwd, fingerprintHash, inference) {
100
101
  }
101
102
  function heuristicInference(fingerprint) {
102
103
  const scripts = fingerprint.rootPackage.scripts || {};
103
- const scriptNames = Object.keys(scripts);
104
- const openapiScriptName = scriptNames.find((s) => s === "openapi:export" || s === "openapi:generate");
105
- const openapiExport = openapiScriptName ? `npm run ${openapiScriptName}` : "npm run openapi:export";
106
- const firstOpenapi = fingerprint.foundPaths.openapi[0];
107
- const firstDocsite = fingerprint.foundPaths.docusaurusConfig[0]
108
- ? node_path_1.default.dirname(fingerprint.foundPaths.docusaurusConfig[0]).replace(/\\/g, "/")
109
- : fingerprint.foundPaths.docsDirs[0]
110
- ? node_path_1.default.dirname(fingerprint.foundPaths.docsDirs[0]).replace(/\\/g, "/")
111
- : "apps/docs-site";
112
- const published = firstOpenapi && firstOpenapi.includes(firstDocsite)
104
+ const fp = fingerprint.foundPaths;
105
+ const exportScript = fp.exportScript;
106
+ const openapiExport = exportScript
107
+ ? `npm run ${exportScript.scriptName}`
108
+ : "npm run openapi:export";
109
+ const firstOpenapi = fp.openapi[0];
110
+ const generatedPath = exportScript?.inferredOutputPath ??
111
+ (firstOpenapi && !node_path_1.default.dirname(firstOpenapi).includes("docs") ? firstOpenapi : "openapi/generated.json");
112
+ const firstDocsite = fp.docusaurusConfig[0]
113
+ ? node_path_1.default.dirname(fp.docusaurusConfig[0]).replace(/\\/g, "/")
114
+ : fp.mkdocs[0]
115
+ ? node_path_1.default.dirname(fp.mkdocs[0]).replace(/\\/g, "/")
116
+ : fp.vitepressConfig?.[0]
117
+ ? node_path_1.default.dirname(fp.vitepressConfig[0]).replace(/\\/g, "/")
118
+ : fp.nextConfig?.[0]
119
+ ? node_path_1.default.dirname(fp.nextConfig[0]).replace(/\\/g, "/")
120
+ : fp.docsDirParents[0]
121
+ ? fp.docsDirParents[0].replace(/\\/g, "/")
122
+ : fp.docsDirs[0]
123
+ ? node_path_1.default.dirname(fp.docsDirs[0]).replace(/\\/g, "/") || undefined
124
+ : undefined;
125
+ const published = firstOpenapi && firstDocsite && firstOpenapi.includes(firstDocsite)
113
126
  ? firstOpenapi
114
- : `${firstDocsite}/openapi/openapi.json`;
115
- const generated = firstOpenapi && !firstOpenapi.includes(firstDocsite)
116
- ? firstOpenapi
117
- : "openapi/generated.json";
127
+ : firstDocsite
128
+ ? `${firstDocsite}/openapi/openapi.json`
129
+ : firstOpenapi ?? "openapi/openapi.json";
118
130
  const verificationCommands = [];
119
131
  if (scripts["docs:gen"])
120
132
  verificationCommands.push("npm run docs:gen");
@@ -122,29 +134,88 @@ function heuristicInference(fingerprint) {
122
134
  verificationCommands.push("npm run docs:build");
123
135
  if (verificationCommands.length === 0)
124
136
  verificationCommands.push("npm run build");
125
- const treeKeys = Object.keys(fingerprint.fileTree);
126
- const hasAppsApi = treeKeys.some((k) => k === "apps/api" || k.startsWith("apps/api/"));
127
- const matchGlob = hasAppsApi ? "apps/api/**" : "**/api/**";
128
- const allowlist = treeKeys.some((k) => k === "apps" || k.startsWith("apps/"))
129
- ? ["openapi/**", "apps/**"]
130
- : ["openapi/**", `${firstDocsite}/**`];
131
- const requireHumanReview = fingerprint.foundPaths.docsDirs.length > 0
137
+ const apiDir = fp.apiDirs[0];
138
+ const matchGlob = apiDir ? `${apiDir}/**` : "**/api/**";
139
+ const allowlistParts = ["openapi/**"];
140
+ if (firstDocsite)
141
+ allowlistParts.push(`${firstDocsite}/**`);
142
+ if (firstOpenapi) {
143
+ const openapiDir = node_path_1.default.dirname(firstOpenapi).replace(/\\/g, "/");
144
+ if (openapiDir && openapiDir !== "." && !allowlistParts.includes(`${openapiDir}/**`)) {
145
+ allowlistParts.push(`${openapiDir}/**`);
146
+ }
147
+ }
148
+ const allowlist = allowlistParts;
149
+ const requireHumanReview = firstDocsite && (fp.docsDirs.length > 0 || fp.docusaurusConfig.length > 0)
132
150
  ? [`${firstDocsite}/docs/guides/**`]
133
151
  : [];
152
+ const pathMappings = firstDocsite || apiDir
153
+ ? [
154
+ {
155
+ match: matchGlob,
156
+ impacts: firstDocsite
157
+ ? [`${firstDocsite}/docs/**`, `${firstDocsite}/openapi/**`]
158
+ : ["**/docs/**", "**/openapi/**"],
159
+ },
160
+ ]
161
+ : [];
162
+ const choices = [
163
+ {
164
+ key: "specProviders.0.current.command",
165
+ question: "OpenAPI export command",
166
+ options: [{ value: openapiExport, label: openapiExport, recommended: true }],
167
+ defaultIndex: 0,
168
+ help: "Use the npm script that generates the spec (e.g. npm run openapi:export).",
169
+ confidence: "medium",
170
+ },
171
+ ];
172
+ if (!firstDocsite) {
173
+ choices.push({
174
+ key: "docsite",
175
+ question: "Docsite path",
176
+ options: [{ value: "", label: "(specify path to docs site root)", recommended: false }],
177
+ defaultIndex: 0,
178
+ help: "Path to Docusaurus, MkDocs, VitePress, or other docs site root.",
179
+ confidence: "low",
180
+ });
181
+ }
182
+ else {
183
+ choices.push({
184
+ key: "docsite",
185
+ question: "Docsite path",
186
+ options: [{ value: firstDocsite, label: firstDocsite, recommended: true }],
187
+ defaultIndex: 0,
188
+ confidence: "medium",
189
+ });
190
+ }
191
+ if (!apiDir) {
192
+ choices.push({
193
+ key: "pathMappings.0.match",
194
+ question: "API/source code path (pathMappings.match)",
195
+ options: [{ value: "**/api/**", label: "**/api/** (generic)", recommended: true }],
196
+ defaultIndex: 0,
197
+ help: "Glob for API or source code that, when changed, may require doc updates.",
198
+ confidence: "low",
199
+ });
200
+ }
134
201
  return {
135
202
  suggestedConfig: {
136
203
  version: 2,
137
204
  specProviders: [
138
205
  {
139
206
  format: "openapi3",
140
- current: { type: "export", command: openapiExport, outputPath: generated },
207
+ current: {
208
+ type: "export",
209
+ command: openapiExport,
210
+ outputPath: generatedPath,
211
+ },
141
212
  published,
142
213
  },
143
214
  ],
144
- docsite: firstDocsite,
215
+ ...(firstDocsite ? { docsite: firstDocsite } : {}),
145
216
  exclude: ["**/CHANGELOG*", "**/blog/**"],
146
217
  requireHumanReview,
147
- pathMappings: [{ match: matchGlob, impacts: [`${firstDocsite}/docs/**`, `${firstDocsite}/openapi/**`] }],
218
+ pathMappings,
148
219
  mode: "strict",
149
220
  devin: { apiVersion: "v1", unlisted: true, maxAcuLimit: 2, tags: ["docdrift"] },
150
221
  policy: {
@@ -157,23 +228,7 @@ function heuristicInference(fingerprint) {
157
228
  allowNewFiles: false,
158
229
  },
159
230
  },
160
- choices: [
161
- {
162
- key: "specProviders.0.current.command",
163
- question: "OpenAPI export command",
164
- options: [{ value: openapiExport, label: openapiExport, recommended: true }],
165
- defaultIndex: 0,
166
- help: "Use the npm script that generates the spec (e.g. npm run openapi:export).",
167
- confidence: "medium",
168
- },
169
- {
170
- key: "docsite",
171
- question: "Docsite path",
172
- options: [{ value: firstDocsite, label: firstDocsite, recommended: true }],
173
- defaultIndex: 0,
174
- confidence: "medium",
175
- },
176
- ],
231
+ choices,
177
232
  skipQuestions: [],
178
233
  };
179
234
  }
@@ -36,6 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.runSetupLocal = runSetupLocal;
39
40
  exports.runSetupDevin = runSetupDevin;
40
41
  exports.runSetupDevinAndValidate = runSetupDevinAndValidate;
41
42
  const node_path_1 = __importDefault(require("node:path"));
@@ -47,6 +48,9 @@ const setup_prompt_1 = require("./setup-prompt");
47
48
  const generate_yaml_1 = require("./generate-yaml");
48
49
  const index_1 = require("../index");
49
50
  const onboard_1 = require("./onboard");
51
+ const repo_fingerprint_1 = require("./repo-fingerprint");
52
+ const ai_infer_1 = require("./ai-infer");
53
+ const interactive_form_1 = require("./interactive-form");
50
54
  /** Resolve path to docdrift.schema.json in the package */
51
55
  function getSchemaPath() {
52
56
  // dist/src/setup -> ../../../ ; src/setup (tsx) -> ../..
@@ -60,6 +64,43 @@ function getSchemaPath() {
60
64
  }
61
65
  return schemaPath;
62
66
  }
67
+ /** Generate docdrift.yaml from repo fingerprint + heuristic (no Devin). */
68
+ async function runSetupLocal(options) {
69
+ const cwd = options.cwd ?? process.cwd();
70
+ const outputPath = node_path_1.default.resolve(cwd, options.outputPath ?? "docdrift.yaml");
71
+ const configExists = node_fs_1.default.existsSync(outputPath);
72
+ if (configExists && !options.force) {
73
+ const { confirm } = await Promise.resolve().then(() => __importStar(require("@inquirer/prompts")));
74
+ const overwrite = await confirm({
75
+ message: "docdrift.yaml already exists. Overwrite?",
76
+ default: false,
77
+ });
78
+ if (!overwrite) {
79
+ throw new Error("Setup cancelled.");
80
+ }
81
+ }
82
+ process.stdout.write("Scanning repo…\n");
83
+ const fingerprint = (0, repo_fingerprint_1.buildRepoFingerprint)(cwd);
84
+ const inference = await (0, ai_infer_1.inferConfigFromFingerprint)(fingerprint, cwd);
85
+ process.stdout.write("Inferred config from repo layout. Adjust if needed.\n");
86
+ const formResult = await (0, interactive_form_1.runInteractiveForm)(inference, cwd);
87
+ const config = (0, generate_yaml_1.buildConfigFromInference)(inference, formResult);
88
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(outputPath), { recursive: true });
89
+ (0, generate_yaml_1.writeConfig)(config, outputPath);
90
+ const yamlContent = node_fs_1.default.readFileSync(outputPath, "utf8");
91
+ (0, onboard_1.runOnboarding)(cwd, formResult.onboarding);
92
+ const validation = (0, generate_yaml_1.validateGeneratedConfig)(outputPath);
93
+ if (!validation.ok) {
94
+ throw new Error("Generated config failed validation:\n" + validation.errors.join("\n"));
95
+ }
96
+ return {
97
+ docdriftYaml: yamlContent,
98
+ docDriftMd: formResult.onboarding.addCustomInstructions ? "(created)" : undefined,
99
+ workflowYml: formResult.onboarding.addWorkflow ? "(added)" : undefined,
100
+ summary: "Generated from repo fingerprint (local detection, no Devin).",
101
+ sessionUrl: "",
102
+ };
103
+ }
63
104
  function parseSetupOutput(session) {
64
105
  const raw = session?.structured_output ?? session?.data?.structured_output;
65
106
  if (!raw || typeof raw !== "object")
@@ -67,20 +67,10 @@ function setByKey(obj, key, value) {
67
67
  }
68
68
  cur[parts[parts.length - 1]] = value;
69
69
  }
70
+ /** Structural defaults only; path fields (docsite, specProviders, allowlist paths) come from inference. */
70
71
  const DEFAULT_CONFIG = {
71
72
  version: 2,
72
- specProviders: [
73
- {
74
- format: "openapi3",
75
- current: {
76
- type: "export",
77
- command: "npm run openapi:export",
78
- outputPath: "openapi/generated.json",
79
- },
80
- published: "apps/docs-site/openapi/openapi.json",
81
- },
82
- ],
83
- docsite: "apps/docs-site",
73
+ specProviders: [],
84
74
  exclude: [],
85
75
  requireHumanReview: [],
86
76
  pathMappings: [],
@@ -94,8 +84,8 @@ const DEFAULT_CONFIG = {
94
84
  policy: {
95
85
  prCaps: { maxPrsPerDay: 5, maxFilesTouched: 30 },
96
86
  confidence: { autopatchThreshold: 0.8 },
97
- allowlist: ["openapi/**", "apps/**"],
98
- verification: { commands: ["npm run docs:gen", "npm run docs:build"] },
87
+ allowlist: ["openapi/**"],
88
+ verification: { commands: ["npm run build"] },
99
89
  slaDays: 7,
100
90
  slaLabel: "docdrift",
101
91
  allowNewFiles: false,
@@ -1,20 +1,106 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
39
  exports.runSetup = runSetup;
7
40
  const node_path_1 = __importDefault(require("node:path"));
41
+ const prompts_1 = require("@inquirer/prompts");
8
42
  const devin_setup_1 = require("./devin-setup");
43
+ /** Ask user whether repo is set up with Devin; if not, we use local (manual) setup. */
44
+ async function chooseSetupMode() {
45
+ if (!process.stdin.isTTY) {
46
+ return "local";
47
+ }
48
+ const choice = await (0, prompts_1.select)({
49
+ message: "Is this repo already set up with Devin? (e.g. added in Devin's Machine)",
50
+ choices: [
51
+ {
52
+ name: "No — use local setup (scan repo, answer a few questions)",
53
+ value: "local",
54
+ },
55
+ {
56
+ name: "Yes — use Devin to generate config (requires repo in Devin + DEVIN_API_KEY)",
57
+ value: "devin",
58
+ },
59
+ ],
60
+ });
61
+ return choice;
62
+ }
9
63
  async function runSetup(options = {}) {
10
64
  const cwd = options.cwd ?? process.cwd();
11
65
  const outputPath = node_path_1.default.resolve(cwd, options.outputPath ?? "docdrift.yaml");
12
- const result = await (0, devin_setup_1.runSetupDevinAndValidate)({
13
- cwd,
14
- outputPath: options.outputPath ?? "docdrift.yaml",
15
- force: options.force,
16
- openPr: options.openPr,
17
- });
66
+ const mode = await chooseSetupMode();
67
+ const hasDevinKey = Boolean(process.env.DEVIN_API_KEY?.trim());
68
+ let result;
69
+ let usedLocalFallback = false;
70
+ if (mode === "local" || (mode === "devin" && !hasDevinKey)) {
71
+ if (mode === "devin" && !hasDevinKey) {
72
+ console.log("\nDEVIN_API_KEY is not set. Using local setup instead.\n");
73
+ }
74
+ result = await (0, devin_setup_1.runSetupLocal)({
75
+ cwd,
76
+ outputPath: options.outputPath ?? "docdrift.yaml",
77
+ force: options.force,
78
+ });
79
+ }
80
+ else {
81
+ try {
82
+ result = await (0, devin_setup_1.runSetupDevinAndValidate)({
83
+ cwd,
84
+ outputPath: options.outputPath ?? "docdrift.yaml",
85
+ force: options.force,
86
+ openPr: options.openPr,
87
+ });
88
+ }
89
+ catch (err) {
90
+ console.error("\nDevin setup failed:", err instanceof Error ? err.message : String(err));
91
+ console.log("\nFalling back to local detection (repo fingerprint + heuristic)…\n");
92
+ usedLocalFallback = true;
93
+ result = await (0, devin_setup_1.runSetupLocal)({
94
+ cwd,
95
+ outputPath: options.outputPath ?? "docdrift.yaml",
96
+ force: options.force,
97
+ });
98
+ }
99
+ }
100
+ if (outputPath === node_path_1.default.resolve(cwd, "docdrift.yaml")) {
101
+ const { runValidate } = await Promise.resolve().then(() => __importStar(require("../index")));
102
+ await runValidate();
103
+ }
18
104
  console.log("\ndocdrift setup complete\n");
19
105
  console.log(" docdrift.yaml written and validated");
20
106
  if (result.docDriftMd)
@@ -25,11 +111,28 @@ async function runSetup(options = {}) {
25
111
  }
26
112
  console.log(" .gitignore updated");
27
113
  console.log("\nSummary: " + result.summary);
28
- console.log("\nSession: " + result.sessionUrl);
114
+ if (result.sessionUrl)
115
+ console.log("\nSession: " + result.sessionUrl);
29
116
  console.log("\nNext steps:");
30
- console.log(" 1. Add DEVIN_API_KEY to repo secrets (Settings > Secrets > Actions)");
31
- console.log(" 2. Ensure your repo is set up in Devin (Devin's Machine > Add repository)");
32
- console.log(" 3. Run: npx @devinnn/docdrift validate — verify config");
33
- console.log(" 4. Run: npx @devinnn/docdrift detect — check for drift");
34
- console.log(" 5. Run: npx @devinnn/docdrift run create Devin session (requires DEVIN_API_KEY)");
117
+ const usedLocal = mode === "local" || (mode === "devin" && !hasDevinKey) || usedLocalFallback;
118
+ if (usedLocal) {
119
+ console.log(" 1. Run: npx @devinnn/docdrift validate — verify config");
120
+ console.log(" 2. Run: npx @devinnn/docdrift detect — check for drift");
121
+ if (usedLocalFallback) {
122
+ console.log(" 3. (Optional) Fix Devin and run setup again, or keep using local config");
123
+ }
124
+ else if (mode === "local") {
125
+ console.log(" 3. (Optional) Add repo to Devin and set DEVIN_API_KEY to use Devin for setup next time");
126
+ }
127
+ else {
128
+ console.log(" 3. (Optional) Set DEVIN_API_KEY and run setup again to use Devin");
129
+ }
130
+ }
131
+ else {
132
+ console.log(" 1. Add DEVIN_API_KEY to repo secrets (Settings > Secrets > Actions)");
133
+ console.log(" 2. Ensure your repo is set up in Devin (Devin's Machine > Add repository)");
134
+ console.log(" 3. Run: npx @devinnn/docdrift validate — verify config");
135
+ console.log(" 4. Run: npx @devinnn/docdrift detect — check for drift");
136
+ console.log(" 5. Run: npx @devinnn/docdrift run — create Devin session (requires DEVIN_API_KEY)");
137
+ }
35
138
  }
@@ -7,7 +7,11 @@ exports.SYSTEM_PROMPT = `You are a docdrift config expert. Given a repo fingerpr
7
7
 
8
8
  Minimal valid config uses: version: 2, specProviders (or pathMappings only for path-only setups), docsite, devin, policy.
9
9
 
10
- Example:
10
+ Use paths from the fingerprint only. Do not invent or assume paths. If docsite or API/source path cannot be determined, add them to choices so the user can specify.
11
+
12
+ Common repo layouts: packages/api + packages/docs, apps/api + apps/docs-site, docs/ at root, openapi/ at root, etc. Infer from foundPaths (docusaurusConfig, mkdocs, vitepressConfig, nextConfig, docsDirs, docsDirParents, openapi, exportScript, apiDirs).
13
+
14
+ Example (replace {docsitePath} and {apiDir} with actual paths from the fingerprint):
11
15
  \`\`\`yaml
12
16
  version: 2
13
17
  specProviders:
@@ -16,12 +20,12 @@ specProviders:
16
20
  type: export
17
21
  command: "npm run openapi:export"
18
22
  outputPath: "openapi/generated.json"
19
- published: "apps/docs-site/openapi/openapi.json"
20
- docsite: "apps/docs-site"
23
+ published: "{docsitePath}/openapi/openapi.json"
24
+ docsite: "{docsitePath}"
21
25
  pathMappings:
22
- - match: "apps/api/**"
23
- impacts: ["apps/docs-site/docs/**", "apps/docs-site/openapi/**"]
24
- exclude: ["**/CHANGELOG*", "apps/docs-site/blog/**"]
26
+ - match: "{apiDir}/**"
27
+ impacts: ["{docsitePath}/docs/**", "{docsitePath}/openapi/**"]
28
+ exclude: ["**/CHANGELOG*", "**/blog/**"]
25
29
  requireHumanReview: []
26
30
  mode: strict
27
31
  devin:
@@ -32,7 +36,7 @@ devin:
32
36
  policy:
33
37
  prCaps: { maxPrsPerDay: 5, maxFilesTouched: 30 }
34
38
  confidence: { autopatchThreshold: 0.8 }
35
- allowlist: ["openapi/**", "apps/**"]
39
+ allowlist: ["openapi/**", "{docsitePath}/**"]
36
40
  verification:
37
41
  commands: ["npm run docs:gen", "npm run docs:build"]
38
42
  slaDays: 7
@@ -43,87 +47,29 @@ policy:
43
47
  ## Field rules
44
48
 
45
49
  - version: Always use 2.
46
- - specProviders: Array of spec sources. For OpenAPI: format "openapi3", current.type "export", current.command = npm script (e.g. "npm run openapi:export"), current.outputPath = where export writes (e.g. "openapi/generated.json"), published = docsite path (e.g. "apps/docs-site/openapi/openapi.json"). Never use raw script body; use "npm run <scriptName>".
47
- - docsite: Path to the docs site root (Docusaurus, Next.js docs, VitePress, MkDocs). Single string or array of strings.
48
- - pathMappings: Array of { match, impacts }. match = glob for source/API code; impacts = globs for doc files that may need updates when match changes.
49
- - mode: "strict" (only run on spec drift) or "auto" (also run when pathMappings match without spec drift). Default: strict.
50
- - policy.verification.commands: Commands to run after patching (e.g. "npm run docs:gen", "npm run docs:build"). Must exist in repo.
50
+ - specProviders: Use paths from fingerprint. current.command = npm script from root or workspace (e.g. from foundPaths.exportScript or scripts containing openapi/swagger/spec). current.outputPath = where export writes (from exportScript.inferredOutputPath or openapi paths). published = path under docsite (e.g. {docsitePath}/openapi/openapi.json). Never use raw script body; use "npm run <scriptName>".
51
+ - docsite: Path from fingerprint (docusaurusConfig dir, mkdocs dir, vitepressConfig dir, nextConfig dir, or docsDirParents). If missing, add to choices.
52
+ - pathMappings: match = API/source dir from fingerprint (apiDirs[0] or exportScript.inferredApiDir), or "**/api/**" if unknown. impacts = docsite docs and openapi globs.
53
+ - mode: "strict" or "auto". Default: strict.
54
+ - policy.verification.commands: Commands that exist in repo (from rootPackage.scripts).
51
55
  - exclude: Globs to never touch (e.g. blog, CHANGELOG).
52
- - requireHumanReview: Globs that require human review when touched (e.g. guides).
56
+ - requireHumanReview: Globs for guides (e.g. {docsitePath}/docs/guides/**).
53
57
 
54
58
  ## Path-only config (no OpenAPI)
55
59
 
56
- If no OpenAPI/spec found, use version: 2 with pathMappings only (no specProviders):
57
- \`\`\`yaml
58
- version: 2
59
- docsite: "apps/docs-site"
60
- pathMappings: [...]
61
- mode: auto
62
- \`\`\`
60
+ If no OpenAPI/spec found, use version: 2 with pathMappings only (no specProviders). Use docsite path from fingerprint or add to choices.
63
61
 
64
62
  ## Common patterns
65
63
 
66
- - Docusaurus: docsite often has docusaurus.config.*; docs:gen may be "docusaurus -- gen-api-docs api"; published path often under docsite/openapi/.
67
- - Next/VitePress/MkDocs: docsite is the app root; look for docs/ or similar.
64
+ - Docusaurus: foundPaths.docusaurusConfig; docsite = dir of config; published often under docsite/openapi/.
65
+ - MkDocs/VitePress/Next: docsite = dir of mkdocs.yml or vitepress.config.* or next.config.*.
66
+ - Generic: docsDirParents or dir containing docs/.
68
67
 
69
68
  ## Output rules
70
69
 
71
- 1. Infer suggestedConfig from the fingerprint. Use version: 2. Only include fields you can confidently infer. Use existing paths and scripts from the fingerprint; do not invent paths that are not present.
72
- 2. For each field where confidence is medium or low, OR where multiple valid options exist, add an entry to choices with: key (e.g. "specProviders.0.current.command"), question, options (array of { value, label, recommended? }), defaultIndex, help?, warning?, confidence ("high"|"medium"|"low").
73
- 3. Add to skipQuestions the keys for which you are highly confident so the CLI will not ask the user.
74
- 4. Prefer fewer, high-quality choices. If truly uncertain, set confidence to "low" and provide 2–3 options.
75
- 5. Do not suggest paths that do not exist in the fingerprint. Prefer existing package.json scripts for export and verification commands.
76
- 6. suggestedConfig must be a valid partial docdrift config; policy.allowlist and policy.verification.commands are required if you include policy. devin.apiVersion must be "v1" if you include devin.
77
-
78
- ## Example docdrift.yaml
79
- # yaml-language-server: $schema=./docdrift.schema.json
80
- version: 2
81
-
82
- specProviders:
83
- - format: openapi3
84
- current:
85
- type: export
86
- command: "npm run openapi:export"
87
- outputPath: "openapi/generated.json"
88
- published: "apps/docs-site/openapi/openapi.json"
89
-
90
- docsite: "apps/docs-site"
91
- mode: strict
92
-
93
- pathMappings:
94
- - match: "apps/api/**"
95
- impacts: ["apps/docs-site/docs/**", "apps/docs-site/openapi/**"]
96
-
97
- exclude:
98
- - "apps/docs-site/blog/**"
99
- - "**/CHANGELOG*"
100
-
101
- requireHumanReview:
102
- - "apps/docs-site/docs/guides/**"
103
-
104
- devin:
105
- apiVersion: v1
106
- unlisted: true
107
- maxAcuLimit: 2
108
- tags:
109
- - docdrift
110
- customInstructions:
111
- - "DocDrift.md"
112
-
113
- policy:
114
- prCaps:
115
- maxPrsPerDay: 5
116
- maxFilesTouched: 30
117
- confidence:
118
- autopatchThreshold: 0.8
119
- allowlist:
120
- - "openapi/**"
121
- - "apps/**"
122
- verification:
123
- commands:
124
- - "npm run docs:gen"
125
- - "npm run docs:build"
126
- slaDays: 7
127
- slaLabel: docdrift
128
- allowNewFiles: false
70
+ 1. Infer suggestedConfig from the fingerprint. Use only paths and script names that appear in the fingerprint. Do not invent paths.
71
+ 2. For each field where confidence is medium or low, or path cannot be inferred, add an entry to choices (e.g. docsite, pathMappings.0.match).
72
+ 3. Add to skipQuestions the keys for which you are highly confident.
73
+ 4. If docsite or API path cannot be determined, add to choices so the user can specify.
74
+ 5. suggestedConfig must be a valid partial docdrift config; policy.allowlist and policy.verification.commands are required if you include policy. devin.apiVersion must be "v1" if you include devin.
129
75
  `;
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.inferExportFromScript = inferExportFromScript;
6
7
  exports.buildRepoFingerprint = buildRepoFingerprint;
7
8
  exports.fingerprintHash = fingerprintHash;
8
9
  const node_crypto_1 = __importDefault(require("node:crypto"));
@@ -93,6 +94,103 @@ function findDirsNamed(cwd, name) {
93
94
  scan(cwd, 0);
94
95
  return out;
95
96
  }
97
+ /** Infer API dir and optional output path from a script string (e.g. "tsx apps/api/scripts/export-openapi.ts"). */
98
+ function inferExportFromScript(script, cwd) {
99
+ const result = {};
100
+ // Match tsx/node/npx path/to/file.ts or .js
101
+ const fileMatch = script.match(/\b(?:tsx|node|npx)\s+(.+?\.(?:ts|js|mjs|cjs))(?:\s|$)/);
102
+ if (fileMatch) {
103
+ const filePath = fileMatch[1].trim().replace(/\\/g, "/");
104
+ const absPath = node_path_1.default.isAbsolute(filePath) ? filePath : node_path_1.default.resolve(cwd, filePath);
105
+ const relPath = node_path_1.default.relative(cwd, absPath).replace(/\\/g, "/");
106
+ const parts = relPath.split("/");
107
+ if (parts.length >= 2 && parts[parts.length - 1].toLowerCase().includes("export")) {
108
+ const dir = node_path_1.default.dirname(relPath);
109
+ if (dir.endsWith("/scripts") || dir.endsWith("scripts")) {
110
+ result.apiDir = node_path_1.default.dirname(dir);
111
+ }
112
+ else {
113
+ result.apiDir = dir;
114
+ }
115
+ }
116
+ else if (parts.length >= 1) {
117
+ result.apiDir = node_path_1.default.dirname(relPath) || ".";
118
+ }
119
+ if (node_fs_1.default.existsSync(absPath)) {
120
+ try {
121
+ const content = node_fs_1.default.readFileSync(absPath, "utf8");
122
+ const outMatch = content.match(/outputPath\s*[=:]\s*["'`]([^"'`]+)["'`]/);
123
+ if (outMatch)
124
+ result.outputPath = outMatch[1];
125
+ }
126
+ catch {
127
+ // ignore
128
+ }
129
+ }
130
+ }
131
+ return result;
132
+ }
133
+ const EXPORT_SCRIPT_NAMES = [
134
+ "openapi:export",
135
+ "openapi:generate",
136
+ "openapi:build",
137
+ "spec:export",
138
+ "spec:generate",
139
+ ];
140
+ const EXPORT_SCRIPT_PATTERN = /(openapi|swagger|spec).*(export|generate|build)/i;
141
+ function findExportScript(scripts, cwd) {
142
+ const name = Object.keys(scripts).find((k) => EXPORT_SCRIPT_NAMES.includes(k)) ??
143
+ Object.keys(scripts).find((k) => EXPORT_SCRIPT_PATTERN.test(k));
144
+ if (!name)
145
+ return undefined;
146
+ const script = scripts[name];
147
+ if (!script || typeof script !== "string")
148
+ return undefined;
149
+ const { outputPath, apiDir } = inferExportFromScript(script, cwd);
150
+ return { scriptName: name, script, inferredApiDir: apiDir, inferredOutputPath: outputPath };
151
+ }
152
+ function collectApiDirCandidates(fileTree, exportScriptApiDir, workspacePackages) {
153
+ const candidates = [];
154
+ const seen = new Set();
155
+ if (exportScriptApiDir && !seen.has(exportScriptApiDir)) {
156
+ candidates.push(exportScriptApiDir);
157
+ seen.add(exportScriptApiDir);
158
+ }
159
+ const roots = ["packages", "apps", "libs", "services"];
160
+ for (const [relDir, names] of Object.entries(fileTree)) {
161
+ const parts = relDir.split("/").filter(Boolean);
162
+ const top = parts[0];
163
+ if (top && roots.includes(top) && names.some((n) => n === "api/" || n === "server/" || n === "backend/")) {
164
+ for (const name of names) {
165
+ if (name === "api/" || name === "server/" || name === "backend/") {
166
+ const dir = parts.length > 0 ? `${relDir}/${name.replace(/\/$/, "")}` : name.replace(/\/$/, "");
167
+ if (!seen.has(dir)) {
168
+ candidates.push(dir);
169
+ seen.add(dir);
170
+ }
171
+ }
172
+ }
173
+ }
174
+ const lower = relDir.toLowerCase();
175
+ if ((lower.endsWith("/api") || lower.endsWith("/server") || lower.endsWith("/backend")) && !seen.has(relDir)) {
176
+ candidates.push(relDir);
177
+ seen.add(relDir);
178
+ }
179
+ }
180
+ for (const wp of workspacePackages) {
181
+ const pkgPath = wp.path.replace(/\\/g, "/");
182
+ if (pkgPath.toLowerCase().includes("api") && !seen.has(pkgPath)) {
183
+ candidates.push(pkgPath);
184
+ seen.add(pkgPath);
185
+ }
186
+ const treeEntry = fileTree[pkgPath];
187
+ if (treeEntry?.some((n) => n === "routes/" || n === "controllers/" || n === "src/") && !seen.has(pkgPath)) {
188
+ candidates.push(pkgPath);
189
+ seen.add(pkgPath);
190
+ }
191
+ }
192
+ return candidates;
193
+ }
96
194
  function buildRepoFingerprint(cwd = process.cwd()) {
97
195
  const fileTree = {};
98
196
  walkDir(cwd, 0, fileTree);
@@ -137,16 +235,68 @@ function buildRepoFingerprint(cwd = process.cwd()) {
137
235
  }
138
236
  }
139
237
  }
140
- const openapi = findMatchingFiles(cwd, (_, name) => /^openapi.*\.json$/i.test(name));
141
- const swagger = findMatchingFiles(cwd, (_, name) => /^swagger.*\.json$/i.test(name));
238
+ const openapi = findMatchingFiles(cwd, (rel, name) => {
239
+ if (/^openapi.*\.(json|yaml|yml)$/i.test(name))
240
+ return true;
241
+ if (/^(api-spec|spec)\.(json|yaml|yml)$/i.test(name))
242
+ return true;
243
+ return false;
244
+ });
245
+ const openapiDirSpecs = findMatchingFiles(cwd, (rel) => {
246
+ const norm = rel.replace(/\\/g, "/");
247
+ return ((norm.startsWith("openapi/") && (norm.endsWith("openapi.json") || norm.endsWith("generated.json") || norm.endsWith("published.json"))) ||
248
+ norm === "openapi/openapi.json" ||
249
+ norm === "openapi/generated.json" ||
250
+ norm === "openapi/published.json");
251
+ });
252
+ const allOpenapi = [...openapi];
253
+ for (const p of openapiDirSpecs) {
254
+ if (!allOpenapi.includes(p))
255
+ allOpenapi.push(p);
256
+ }
257
+ const swagger = findMatchingFiles(cwd, (_, name) => /^swagger.*\.(json|yaml|yml)$/i.test(name));
142
258
  const docusaurusConfig = findMatchingFiles(cwd, (_, name) => name.startsWith("docusaurus.config."));
143
259
  const mkdocs = findMatchingFiles(cwd, (_, name) => name === "mkdocs.yml");
260
+ const vitepressConfig = findMatchingFiles(cwd, (_, name) => name.startsWith("vitepress.config."));
261
+ const nextConfig = findMatchingFiles(cwd, (_, name) => name.startsWith("next.config."));
144
262
  const docsDirs = findDirsNamed(cwd, "docs");
263
+ const docsDirParents = [];
264
+ for (const d of docsDirs) {
265
+ const parent = node_path_1.default.dirname(d);
266
+ if (parent && parent !== "." && !docsDirParents.includes(parent))
267
+ docsDirParents.push(parent);
268
+ }
269
+ let exportScript = findExportScript(rootPackage.scripts || {}, cwd);
270
+ if (!exportScript) {
271
+ for (const wp of workspacePackages) {
272
+ const wpCwd = node_path_1.default.join(cwd, wp.path);
273
+ exportScript = findExportScript(wp.scripts || {}, wpCwd);
274
+ if (exportScript) {
275
+ const apiDir = exportScript.inferredApiDir
276
+ ? node_path_1.default.join(wp.path, exportScript.inferredApiDir).replace(/\\/g, "/")
277
+ : wp.path;
278
+ exportScript = { ...exportScript, inferredApiDir: apiDir };
279
+ break;
280
+ }
281
+ }
282
+ }
283
+ const apiDirs = collectApiDirCandidates(fileTree, exportScript?.inferredApiDir, workspacePackages);
145
284
  return {
146
285
  fileTree,
147
286
  rootPackage,
148
287
  workspacePackages,
149
- foundPaths: { openapi, swagger, docusaurusConfig, mkdocs, docsDirs },
288
+ foundPaths: {
289
+ openapi: allOpenapi,
290
+ swagger,
291
+ docusaurusConfig,
292
+ mkdocs,
293
+ vitepressConfig,
294
+ nextConfig,
295
+ docsDirs,
296
+ docsDirParents,
297
+ exportScript,
298
+ apiDirs,
299
+ },
150
300
  };
151
301
  }
152
302
  function fingerprintHash(fingerprint) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devinnn/docdrift",
3
- "version": "0.1.7",
3
+ "version": "0.1.12",
4
4
  "private": false,
5
5
  "description": "Detect and remediate documentation drift with Devin sessions",
6
6
  "main": "dist/src/index.js",
@@ -17,7 +17,11 @@
17
17
  ],
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": ""
20
+ "url": "git+https://github.com/cameronking4/docdrift.git"
21
+ },
22
+ "homepage": "https://github.com/cameronking4/docdrift#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/cameronking4/docdrift/issues"
21
25
  },
22
26
  "keywords": [
23
27
  "docs",
@@ -27,6 +31,9 @@
27
31
  "github-actions"
28
32
  ],
29
33
  "license": "MIT",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
30
37
  "scripts": {
31
38
  "build": "tsc -p tsconfig.json",
32
39
  "test": "vitest run",