@devinnn/docdrift 0.1.4 → 0.1.7
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 +1 -1
- package/dist/src/cli.js +8 -1
- package/dist/src/config/normalize.js +2 -4
- package/dist/src/config/schema.js +4 -4
- package/dist/src/detect/index.js +9 -2
- package/dist/src/devin/prompts.js +19 -5
- package/dist/src/devin/schemas.js +22 -1
- package/dist/src/github/client.js +24 -0
- package/dist/src/index.js +22 -10
- package/dist/src/setup/ai-infer.js +44 -18
- package/dist/src/setup/devin-setup.js +157 -0
- package/dist/src/setup/generate-yaml.js +43 -6
- package/dist/src/setup/index.js +20 -94
- package/dist/src/setup/onboard.js +132 -3
- package/dist/src/setup/prompts.js +81 -14
- package/dist/src/setup/setup-prompt.js +54 -0
- package/docdrift.schema.json +7 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -163,7 +163,7 @@ This repo has **intentional drift**: the API has been expanded (new fields `full
|
|
|
163
163
|
|
|
164
164
|
Once published to npm, any repo can use the CLI locally or in GitHub Actions.
|
|
165
165
|
|
|
166
|
-
1. **
|
|
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
167
|
2. **CLI**
|
|
168
168
|
```bash
|
|
169
169
|
npx @devinnn/docdrift validate
|
package/dist/src/cli.js
CHANGED
|
@@ -50,7 +50,14 @@ function getArg(args, flag) {
|
|
|
50
50
|
async function main() {
|
|
51
51
|
const [, , command, ...args] = process.argv;
|
|
52
52
|
if (!command) {
|
|
53
|
-
throw new Error("Usage: docdrift <validate|detect|run|status|sla-check|setup|generate-yaml> [options]\n
|
|
53
|
+
throw new Error("Usage: docdrift <validate|detect|run|status|sla-check|setup|generate-yaml> [options]\n" +
|
|
54
|
+
" validate Validate docdrift.yaml (v2 config)\n" +
|
|
55
|
+
" detect Check for drift [--base SHA] [--head SHA]\n" +
|
|
56
|
+
" run Full run with Devin [--base SHA] [--head SHA]\n" +
|
|
57
|
+
" status Show run status [--since 24h]\n" +
|
|
58
|
+
" sla-check Check SLA for unmerged PRs\n" +
|
|
59
|
+
" setup Interactive setup (generates v2 docdrift.yaml)\n" +
|
|
60
|
+
" generate-yaml Generate config [--output path] [--force]");
|
|
54
61
|
}
|
|
55
62
|
if (command === "setup" || command === "generate-yaml") {
|
|
56
63
|
require("dotenv").config();
|
|
@@ -16,8 +16,7 @@ function normalizeConfig(config) {
|
|
|
16
16
|
let exclude = config.exclude ?? [];
|
|
17
17
|
let requireHumanReview = config.requireHumanReview ?? [];
|
|
18
18
|
let docAreas = config.docAreas ?? [];
|
|
19
|
-
const
|
|
20
|
-
const inferMode = config.inferMode ?? true;
|
|
19
|
+
const mode = config.mode ?? "strict";
|
|
21
20
|
if (config.specProviders && config.specProviders.length >= 1) {
|
|
22
21
|
specProviders = config.specProviders;
|
|
23
22
|
const firstOpenApi3 = specProviders.find((p) => p.format === "openapi3");
|
|
@@ -149,7 +148,6 @@ function normalizeConfig(config) {
|
|
|
149
148
|
exclude,
|
|
150
149
|
requireHumanReview,
|
|
151
150
|
docAreas,
|
|
152
|
-
|
|
153
|
-
inferMode,
|
|
151
|
+
mode,
|
|
154
152
|
};
|
|
155
153
|
}
|
|
@@ -98,8 +98,8 @@ exports.docDriftConfigBaseSchema = zod_1.z.object({
|
|
|
98
98
|
exclude: zod_1.z.array(zod_1.z.string().min(1)).optional().default([]),
|
|
99
99
|
requireHumanReview: zod_1.z.array(zod_1.z.string().min(1)).optional().default([]),
|
|
100
100
|
pathMappings: zod_1.z.array(exports.pathRuleSchema).optional().default([]),
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
/** strict: only run on spec drift. auto: also run when pathMappings match (no spec drift). */
|
|
102
|
+
mode: zod_1.z.enum(["strict", "auto"]).optional().default("strict"),
|
|
103
103
|
devin: zod_1.z.object({
|
|
104
104
|
apiVersion: zod_1.z.literal("v1"),
|
|
105
105
|
unlisted: zod_1.z.boolean().default(true),
|
|
@@ -119,8 +119,8 @@ const docDriftConfigObjectSchema = zod_1.z.object({
|
|
|
119
119
|
exclude: zod_1.z.array(zod_1.z.string().min(1)).optional().default([]),
|
|
120
120
|
requireHumanReview: zod_1.z.array(zod_1.z.string().min(1)).optional().default([]),
|
|
121
121
|
pathMappings: zod_1.z.array(exports.pathRuleSchema).optional().default([]),
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
/** strict: only run on spec drift. auto: also run when pathMappings match (no spec drift). */
|
|
123
|
+
mode: zod_1.z.enum(["strict", "auto"]).optional().default("strict"),
|
|
124
124
|
devin: zod_1.z.object({
|
|
125
125
|
apiVersion: zod_1.z.literal("v1"),
|
|
126
126
|
unlisted: zod_1.z.boolean().default(true),
|
package/dist/src/detect/index.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.buildDriftReport = buildDriftReport;
|
|
|
7
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
8
|
const fs_1 = require("../utils/fs");
|
|
9
9
|
const git_1 = require("../utils/git");
|
|
10
|
+
const glob_1 = require("../utils/glob");
|
|
10
11
|
const heuristics_1 = require("./heuristics");
|
|
11
12
|
const registry_1 = require("../spec-providers/registry");
|
|
12
13
|
async function buildDriftReport(input) {
|
|
@@ -68,15 +69,21 @@ async function buildDriftReport(input) {
|
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
const hasHeuristicMatch = signals.some((s) => s.kind === "heuristic_path_impact");
|
|
72
|
+
const pathMappings = config.pathMappings ?? [];
|
|
73
|
+
const hasPathMappingMatch = pathMappings.length > 0 &&
|
|
74
|
+
changedPaths.some((p) => pathMappings.some((m) => (0, glob_1.matchesGlob)(m.match, p)));
|
|
71
75
|
// 3. Gate logic
|
|
76
|
+
const isAuto = config.mode === "auto";
|
|
72
77
|
let runGate = "none";
|
|
73
78
|
if (anySpecDrift) {
|
|
74
79
|
runGate = "spec_drift";
|
|
75
80
|
}
|
|
76
|
-
else if (
|
|
81
|
+
else if (isAuto && hasHeuristicMatch) {
|
|
77
82
|
runGate = "conceptual_only";
|
|
78
83
|
}
|
|
79
|
-
else if (
|
|
84
|
+
else if (isAuto &&
|
|
85
|
+
hasPathMappingMatch &&
|
|
86
|
+
(config.specProviders.length === 0 || allSpecFailedOrNoDrift)) {
|
|
80
87
|
runGate = "infer";
|
|
81
88
|
if (config.specProviders.length > 0) {
|
|
82
89
|
for (const r of providerResults) {
|
|
@@ -97,15 +97,29 @@ function buildWholeDocsitePrompt(input) {
|
|
|
97
97
|
"",
|
|
98
98
|
].join("\n")
|
|
99
99
|
: "";
|
|
100
|
-
const draftPrBlock =
|
|
101
|
-
|
|
100
|
+
const draftPrBlock = (() => {
|
|
101
|
+
if (input.trigger !== "pull_request" || !input.prNumber)
|
|
102
|
+
return "";
|
|
103
|
+
if (input.existingDocdriftPr) {
|
|
104
|
+
return [
|
|
105
|
+
"",
|
|
106
|
+
"CRITICAL: An existing doc-drift PR already exists for this API PR.",
|
|
107
|
+
`You MUST UPDATE that PR — do NOT create a new one.`,
|
|
108
|
+
`- Existing PR: #${input.existingDocdriftPr.number} (${input.existingDocdriftPr.url})`,
|
|
109
|
+
`- Branch to update: ${input.existingDocdriftPr.headRef}`,
|
|
110
|
+
"Checkout that branch, pull latest main, apply your doc changes, push. The existing PR will update.",
|
|
111
|
+
"Do NOT open a new pull request.",
|
|
112
|
+
"",
|
|
113
|
+
].join("\n");
|
|
114
|
+
}
|
|
115
|
+
return [
|
|
102
116
|
"",
|
|
103
117
|
"This run was triggered by an open API PR. Open a **draft** pull request.",
|
|
104
118
|
`In the PR description, link to the API PR (#${input.prNumber}) and state: "Merge the API PR first, then review this doc PR."`,
|
|
105
|
-
"Use
|
|
119
|
+
"Use branch name docdrift/pr-" + input.prNumber + " (required for future runs to update this PR).",
|
|
106
120
|
"",
|
|
107
|
-
].join("\n")
|
|
108
|
-
|
|
121
|
+
].join("\n");
|
|
122
|
+
})();
|
|
109
123
|
const pathMappings = input.config.pathMappings ?? [];
|
|
110
124
|
const pathMappingsBlock = pathMappings.length > 0
|
|
111
125
|
? [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PatchResultSchema = exports.PatchPlanSchema = void 0;
|
|
3
|
+
exports.PatchResultSchema = exports.SetupOutputSchema = exports.PatchPlanSchema = void 0;
|
|
4
4
|
exports.PatchPlanSchema = {
|
|
5
5
|
type: "object",
|
|
6
6
|
additionalProperties: false,
|
|
@@ -59,6 +59,27 @@ exports.PatchPlanSchema = {
|
|
|
59
59
|
},
|
|
60
60
|
},
|
|
61
61
|
};
|
|
62
|
+
/** Structured output for docdrift setup (Devin generates config files) */
|
|
63
|
+
exports.SetupOutputSchema = {
|
|
64
|
+
type: "object",
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
required: ["docdriftYaml", "summary"],
|
|
67
|
+
properties: {
|
|
68
|
+
docdriftYaml: { type: "string", description: "Full docdrift.yaml content, valid per schema" },
|
|
69
|
+
docDriftMd: {
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "Content for .docdrift/DocDrift.md custom instructions (project-specific guidance for Devin)",
|
|
72
|
+
},
|
|
73
|
+
workflowYml: {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "Content for .github/workflows/docdrift.yml — must use npx @devinnn/docdrift for validate and run",
|
|
76
|
+
},
|
|
77
|
+
summary: {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "Brief summary of what you inferred (openapi paths, docsite, verification commands)",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
62
83
|
exports.PatchResultSchema = {
|
|
63
84
|
type: "object",
|
|
64
85
|
additionalProperties: false,
|
|
@@ -10,6 +10,7 @@ exports.renderRequireHumanReviewIssueBody = renderRequireHumanReviewIssueBody;
|
|
|
10
10
|
exports.renderSlaIssueBody = renderSlaIssueBody;
|
|
11
11
|
exports.isPrOpen = isPrOpen;
|
|
12
12
|
exports.listOpenPrsWithLabel = listOpenPrsWithLabel;
|
|
13
|
+
exports.findExistingDocdriftPrForSource = findExistingDocdriftPrForSource;
|
|
13
14
|
const rest_1 = require("@octokit/rest");
|
|
14
15
|
function parseRepo(full) {
|
|
15
16
|
const [owner, repo] = full.split("/");
|
|
@@ -172,3 +173,26 @@ async function listOpenPrsWithLabel(token, repository, label) {
|
|
|
172
173
|
created_at: pr.created_at ?? "",
|
|
173
174
|
}));
|
|
174
175
|
}
|
|
176
|
+
/** Find an existing open docdrift PR for a given source PR number.
|
|
177
|
+
* Looks for PRs from branch docdrift/pr-{sourcePrNumber} (Devin's convention).
|
|
178
|
+
* Returns the first match so we can instruct Devin to update it instead of creating a new one.
|
|
179
|
+
*/
|
|
180
|
+
async function findExistingDocdriftPrForSource(token, repository, sourcePrNumber) {
|
|
181
|
+
const octokit = new rest_1.Octokit({ auth: token });
|
|
182
|
+
const { owner, repo } = parseRepo(repository);
|
|
183
|
+
const branchName = `docdrift/pr-${sourcePrNumber}`;
|
|
184
|
+
const { data } = await octokit.pulls.list({
|
|
185
|
+
owner,
|
|
186
|
+
repo,
|
|
187
|
+
state: "open",
|
|
188
|
+
head: branchName,
|
|
189
|
+
});
|
|
190
|
+
const pr = data[0];
|
|
191
|
+
if (!pr)
|
|
192
|
+
return null;
|
|
193
|
+
return {
|
|
194
|
+
number: pr.number,
|
|
195
|
+
url: pr.html_url ?? "",
|
|
196
|
+
headRef: pr.head?.ref ?? branchName,
|
|
197
|
+
};
|
|
198
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -97,6 +97,7 @@ async function executeSessionSingle(input) {
|
|
|
97
97
|
runGate: input.runGate,
|
|
98
98
|
trigger: input.trigger,
|
|
99
99
|
prNumber: input.prNumber,
|
|
100
|
+
existingDocdriftPr: input.existingDocdriftPr,
|
|
100
101
|
});
|
|
101
102
|
const session = await (0, v1_1.devinCreateSession)(input.apiKey, {
|
|
102
103
|
prompt,
|
|
@@ -256,6 +257,16 @@ async function runDocDrift(options) {
|
|
|
256
257
|
}
|
|
257
258
|
const bundle = await (0, bundle_1.buildEvidenceBundle)({ runInfo, item, evidenceRoot });
|
|
258
259
|
const attachmentPaths = [...new Set([bundle.archivePath, ...bundle.attachmentPaths])];
|
|
260
|
+
let existingDocdriftPr;
|
|
261
|
+
if (githubToken && runInfo.trigger === "pull_request" && runInfo.prNumber) {
|
|
262
|
+
existingDocdriftPr = (await (0, client_1.findExistingDocdriftPrForSource)(githubToken, repo, runInfo.prNumber)) ?? undefined;
|
|
263
|
+
if (existingDocdriftPr) {
|
|
264
|
+
(0, log_1.logInfo)("Found existing docdrift PR for source PR; will instruct Devin to update it", {
|
|
265
|
+
existingPr: existingDocdriftPr.number,
|
|
266
|
+
headRef: existingDocdriftPr.headRef,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
259
270
|
let sessionOutcome = {
|
|
260
271
|
outcome: "NO_CHANGE",
|
|
261
272
|
summary: "Skipped Devin session",
|
|
@@ -276,6 +287,7 @@ async function runDocDrift(options) {
|
|
|
276
287
|
runGate,
|
|
277
288
|
trigger: runInfo.trigger,
|
|
278
289
|
prNumber: runInfo.prNumber,
|
|
290
|
+
existingDocdriftPr,
|
|
279
291
|
});
|
|
280
292
|
metrics.timeToSessionTerminalMs.push(Date.now() - sessionStart);
|
|
281
293
|
}
|
|
@@ -296,7 +308,7 @@ async function runDocDrift(options) {
|
|
|
296
308
|
metrics.prsOpened += 1;
|
|
297
309
|
state.lastDocDriftPrUrl = sessionOutcome.prUrl;
|
|
298
310
|
state.lastDocDriftPrOpenedAt = new Date().toISOString();
|
|
299
|
-
if (githubToken && runInfo.trigger === "pull_request" && runInfo.prNumber) {
|
|
311
|
+
if (githubToken && runInfo.trigger === "pull_request" && runInfo.prNumber && !existingDocdriftPr) {
|
|
300
312
|
await (0, client_1.postPrComment)({
|
|
301
313
|
token: githubToken,
|
|
302
314
|
repository: repo,
|
|
@@ -322,19 +334,18 @@ async function runDocDrift(options) {
|
|
|
322
334
|
}
|
|
323
335
|
}
|
|
324
336
|
else if (githubToken &&
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
sessionOutcome.outcome === "NO_CHANGE")) {
|
|
337
|
+
sessionOutcome.outcome === "BLOCKED" &&
|
|
338
|
+
sessionOutcome.summary.includes("DEVIN_API_KEY")) {
|
|
328
339
|
issueUrl = await (0, client_1.createIssue)({
|
|
329
340
|
token: githubToken,
|
|
330
341
|
repository: repo,
|
|
331
342
|
issue: {
|
|
332
|
-
title: "[docdrift]
|
|
343
|
+
title: "[docdrift] Configuration required — set DEVIN_API_KEY",
|
|
333
344
|
body: (0, client_1.renderBlockedIssueBody)({
|
|
334
345
|
docArea: item.docArea,
|
|
335
|
-
evidenceSummary:
|
|
346
|
+
evidenceSummary: sessionOutcome.summary,
|
|
336
347
|
questions: sessionOutcome.questions ?? [
|
|
337
|
-
"
|
|
348
|
+
"Set DEVIN_API_KEY in GitHub Actions secrets or environment.",
|
|
338
349
|
],
|
|
339
350
|
sessionUrl: sessionOutcome.sessionUrl,
|
|
340
351
|
}),
|
|
@@ -342,10 +353,11 @@ async function runDocDrift(options) {
|
|
|
342
353
|
},
|
|
343
354
|
});
|
|
344
355
|
metrics.issuesOpened += 1;
|
|
345
|
-
if (sessionOutcome.outcome !== "PR_OPENED") {
|
|
346
|
-
sessionOutcome.outcome = "ISSUE_OPENED";
|
|
347
|
-
}
|
|
348
356
|
}
|
|
357
|
+
// Note: We do NOT create "docs drift requires input" issues for Devin-reported BLOCKED
|
|
358
|
+
// (evidence questions) or for OPEN_ISSUE/NO_CHANGE. Issues are only created for:
|
|
359
|
+
// (1) requireHumanReview when a PR touches those paths, (2) 7-day SLA reminders,
|
|
360
|
+
// and (3) DEVIN_API_KEY missing. See docdrift-yml.md.
|
|
349
361
|
if (sessionOutcome.outcome === "BLOCKED") {
|
|
350
362
|
metrics.blockedCount += 1;
|
|
351
363
|
}
|
|
@@ -15,20 +15,24 @@ const pathRuleSchema = zod_1.z.object({
|
|
|
15
15
|
match: zod_1.z.string().min(1),
|
|
16
16
|
impacts: zod_1.z.array(zod_1.z.string().min(1)).min(1),
|
|
17
17
|
});
|
|
18
|
+
const specProviderSchema = zod_1.z.object({
|
|
19
|
+
format: zod_1.z.enum(["openapi3", "swagger2", "graphql", "fern", "postman"]),
|
|
20
|
+
current: zod_1.z.object({
|
|
21
|
+
type: zod_1.z.literal("export"),
|
|
22
|
+
command: zod_1.z.string().min(1),
|
|
23
|
+
outputPath: zod_1.z.string().min(1),
|
|
24
|
+
}),
|
|
25
|
+
published: zod_1.z.string().min(1),
|
|
26
|
+
});
|
|
18
27
|
const InferenceSchema = zod_1.z.object({
|
|
19
28
|
suggestedConfig: zod_1.z.object({
|
|
20
|
-
version: zod_1.z.
|
|
21
|
-
|
|
22
|
-
.object({
|
|
23
|
-
export: zod_1.z.string().min(1),
|
|
24
|
-
generated: zod_1.z.string().min(1),
|
|
25
|
-
published: zod_1.z.string().min(1),
|
|
26
|
-
})
|
|
27
|
-
.optional(),
|
|
29
|
+
version: zod_1.z.literal(2).optional(),
|
|
30
|
+
specProviders: zod_1.z.array(specProviderSchema).optional(),
|
|
28
31
|
docsite: zod_1.z.union([zod_1.z.string().min(1), zod_1.z.array(zod_1.z.string().min(1))]).optional(),
|
|
29
32
|
exclude: zod_1.z.array(zod_1.z.string().min(1)).optional(),
|
|
30
33
|
requireHumanReview: zod_1.z.array(zod_1.z.string().min(1)).optional(),
|
|
31
34
|
pathMappings: zod_1.z.array(pathRuleSchema).optional(),
|
|
35
|
+
mode: zod_1.z.enum(["strict", "auto"]).optional(),
|
|
32
36
|
devin: zod_1.z
|
|
33
37
|
.object({
|
|
34
38
|
apiVersion: zod_1.z.literal("v1"),
|
|
@@ -96,15 +100,21 @@ function writeCache(cwd, fingerprintHash, inference) {
|
|
|
96
100
|
}
|
|
97
101
|
function heuristicInference(fingerprint) {
|
|
98
102
|
const scripts = fingerprint.rootPackage.scripts || {};
|
|
99
|
-
const
|
|
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";
|
|
100
106
|
const firstOpenapi = fingerprint.foundPaths.openapi[0];
|
|
101
107
|
const firstDocsite = fingerprint.foundPaths.docusaurusConfig[0]
|
|
102
108
|
? node_path_1.default.dirname(fingerprint.foundPaths.docusaurusConfig[0]).replace(/\\/g, "/")
|
|
103
109
|
: fingerprint.foundPaths.docsDirs[0]
|
|
104
110
|
? node_path_1.default.dirname(fingerprint.foundPaths.docsDirs[0]).replace(/\\/g, "/")
|
|
105
111
|
: "apps/docs-site";
|
|
106
|
-
const published = firstOpenapi
|
|
107
|
-
|
|
112
|
+
const published = firstOpenapi && firstOpenapi.includes(firstDocsite)
|
|
113
|
+
? firstOpenapi
|
|
114
|
+
: `${firstDocsite}/openapi/openapi.json`;
|
|
115
|
+
const generated = firstOpenapi && !firstOpenapi.includes(firstDocsite)
|
|
116
|
+
? firstOpenapi
|
|
117
|
+
: "openapi/generated.json";
|
|
108
118
|
const verificationCommands = [];
|
|
109
119
|
if (scripts["docs:gen"])
|
|
110
120
|
verificationCommands.push("npm run docs:gen");
|
|
@@ -112,19 +122,35 @@ function heuristicInference(fingerprint) {
|
|
|
112
122
|
verificationCommands.push("npm run docs:build");
|
|
113
123
|
if (verificationCommands.length === 0)
|
|
114
124
|
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
|
|
132
|
+
? [`${firstDocsite}/docs/guides/**`]
|
|
133
|
+
: [];
|
|
115
134
|
return {
|
|
116
135
|
suggestedConfig: {
|
|
117
|
-
version:
|
|
118
|
-
|
|
136
|
+
version: 2,
|
|
137
|
+
specProviders: [
|
|
138
|
+
{
|
|
139
|
+
format: "openapi3",
|
|
140
|
+
current: { type: "export", command: openapiExport, outputPath: generated },
|
|
141
|
+
published,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
119
144
|
docsite: firstDocsite,
|
|
120
145
|
exclude: ["**/CHANGELOG*", "**/blog/**"],
|
|
121
|
-
requireHumanReview
|
|
122
|
-
pathMappings: [{ match:
|
|
146
|
+
requireHumanReview,
|
|
147
|
+
pathMappings: [{ match: matchGlob, impacts: [`${firstDocsite}/docs/**`, `${firstDocsite}/openapi/**`] }],
|
|
148
|
+
mode: "strict",
|
|
123
149
|
devin: { apiVersion: "v1", unlisted: true, maxAcuLimit: 2, tags: ["docdrift"] },
|
|
124
150
|
policy: {
|
|
125
151
|
prCaps: { maxPrsPerDay: 5, maxFilesTouched: 30 },
|
|
126
152
|
confidence: { autopatchThreshold: 0.8 },
|
|
127
|
-
allowlist
|
|
153
|
+
allowlist,
|
|
128
154
|
verification: { commands: verificationCommands },
|
|
129
155
|
slaDays: 7,
|
|
130
156
|
slaLabel: "docdrift",
|
|
@@ -133,11 +159,11 @@ function heuristicInference(fingerprint) {
|
|
|
133
159
|
},
|
|
134
160
|
choices: [
|
|
135
161
|
{
|
|
136
|
-
key: "
|
|
162
|
+
key: "specProviders.0.current.command",
|
|
137
163
|
question: "OpenAPI export command",
|
|
138
164
|
options: [{ value: openapiExport, label: openapiExport, recommended: true }],
|
|
139
165
|
defaultIndex: 0,
|
|
140
|
-
help: "
|
|
166
|
+
help: "Use the npm script that generates the spec (e.g. npm run openapi:export).",
|
|
141
167
|
confidence: "medium",
|
|
142
168
|
},
|
|
143
169
|
{
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.runSetupDevin = runSetupDevin;
|
|
40
|
+
exports.runSetupDevinAndValidate = runSetupDevinAndValidate;
|
|
41
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
42
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
43
|
+
require("dotenv/config");
|
|
44
|
+
const v1_1 = require("../devin/v1");
|
|
45
|
+
const schemas_1 = require("../devin/schemas");
|
|
46
|
+
const setup_prompt_1 = require("./setup-prompt");
|
|
47
|
+
const generate_yaml_1 = require("./generate-yaml");
|
|
48
|
+
const index_1 = require("../index");
|
|
49
|
+
const onboard_1 = require("./onboard");
|
|
50
|
+
/** Resolve path to docdrift.schema.json in the package */
|
|
51
|
+
function getSchemaPath() {
|
|
52
|
+
// dist/src/setup -> ../../../ ; src/setup (tsx) -> ../..
|
|
53
|
+
const candidates = [
|
|
54
|
+
node_path_1.default.join(__dirname, "../../../docdrift.schema.json"),
|
|
55
|
+
node_path_1.default.join(__dirname, "../../docdrift.schema.json"),
|
|
56
|
+
];
|
|
57
|
+
const schemaPath = candidates.find((p) => node_fs_1.default.existsSync(p));
|
|
58
|
+
if (!schemaPath) {
|
|
59
|
+
throw new Error(`docdrift.schema.json not found. Tried: ${candidates.join(", ")}`);
|
|
60
|
+
}
|
|
61
|
+
return schemaPath;
|
|
62
|
+
}
|
|
63
|
+
function parseSetupOutput(session) {
|
|
64
|
+
const raw = session?.structured_output ?? session?.data?.structured_output;
|
|
65
|
+
if (!raw || typeof raw !== "object")
|
|
66
|
+
return null;
|
|
67
|
+
const o = raw;
|
|
68
|
+
const yaml = o.docdriftYaml;
|
|
69
|
+
const summary = o.summary;
|
|
70
|
+
if (typeof yaml !== "string" || typeof summary !== "string")
|
|
71
|
+
return null;
|
|
72
|
+
return {
|
|
73
|
+
docdriftYaml: yaml,
|
|
74
|
+
docDriftMd: typeof o.docDriftMd === "string" && o.docDriftMd ? o.docDriftMd : undefined,
|
|
75
|
+
workflowYml: typeof o.workflowYml === "string" && o.workflowYml ? o.workflowYml : undefined,
|
|
76
|
+
summary,
|
|
77
|
+
sessionUrl: "",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function runSetupDevin(options) {
|
|
81
|
+
const cwd = options.cwd ?? process.cwd();
|
|
82
|
+
const outputPath = node_path_1.default.resolve(cwd, options.outputPath ?? "docdrift.yaml");
|
|
83
|
+
const apiKey = process.env.DEVIN_API_KEY?.trim();
|
|
84
|
+
if (!apiKey) {
|
|
85
|
+
throw new Error("DEVIN_API_KEY is required for setup. Set it in .env or export.");
|
|
86
|
+
}
|
|
87
|
+
const configExists = node_fs_1.default.existsSync(outputPath);
|
|
88
|
+
if (configExists && !options.force) {
|
|
89
|
+
const { confirm } = await Promise.resolve().then(() => __importStar(require("@inquirer/prompts")));
|
|
90
|
+
const overwrite = await confirm({
|
|
91
|
+
message: "docdrift.yaml already exists. Overwrite?",
|
|
92
|
+
default: false,
|
|
93
|
+
});
|
|
94
|
+
if (!overwrite) {
|
|
95
|
+
throw new Error("Setup cancelled.");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
process.stdout.write("Uploading schema…\n");
|
|
99
|
+
const schemaPath = getSchemaPath();
|
|
100
|
+
const attachmentUrl = await (0, v1_1.devinUploadAttachment)(apiKey, schemaPath);
|
|
101
|
+
const prompt = (0, setup_prompt_1.buildSetupPrompt)([attachmentUrl]);
|
|
102
|
+
process.stdout.write("Creating Devin session…\n");
|
|
103
|
+
const session = await (0, v1_1.devinCreateSession)(apiKey, {
|
|
104
|
+
prompt,
|
|
105
|
+
unlisted: true,
|
|
106
|
+
max_acu_limit: 2,
|
|
107
|
+
tags: ["docdrift", "setup"],
|
|
108
|
+
attachments: [attachmentUrl],
|
|
109
|
+
structured_output: {
|
|
110
|
+
schema: schemas_1.SetupOutputSchema,
|
|
111
|
+
},
|
|
112
|
+
metadata: { purpose: "docdrift-setup" },
|
|
113
|
+
});
|
|
114
|
+
process.stdout.write("Devin is analyzing the repo and generating config…\n");
|
|
115
|
+
process.stdout.write(`Session: ${session.url}\n`);
|
|
116
|
+
const finalSession = await (0, v1_1.pollUntilTerminal)(apiKey, session.session_id, 15 * 60_000);
|
|
117
|
+
const result = parseSetupOutput(finalSession);
|
|
118
|
+
if (!result) {
|
|
119
|
+
throw new Error("Devin session did not return valid setup output. Check the session for details: " + session.url);
|
|
120
|
+
}
|
|
121
|
+
result.sessionUrl = session.url;
|
|
122
|
+
// Write files
|
|
123
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(outputPath), { recursive: true });
|
|
124
|
+
node_fs_1.default.writeFileSync(outputPath, result.docdriftYaml, "utf8");
|
|
125
|
+
if (result.docDriftMd) {
|
|
126
|
+
(0, onboard_1.ensureDocdriftDir)(cwd);
|
|
127
|
+
const docDriftPath = node_path_1.default.resolve(cwd, ".docdrift", "DocDrift.md");
|
|
128
|
+
node_fs_1.default.writeFileSync(docDriftPath, result.docDriftMd, "utf8");
|
|
129
|
+
}
|
|
130
|
+
if (result.workflowYml) {
|
|
131
|
+
const workflowsDir = node_path_1.default.resolve(cwd, ".github", "workflows");
|
|
132
|
+
node_fs_1.default.mkdirSync(workflowsDir, { recursive: true });
|
|
133
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(workflowsDir, "docdrift.yml"), result.workflowYml, "utf8");
|
|
134
|
+
(0, onboard_1.addSlaCheckWorkflow)(cwd);
|
|
135
|
+
}
|
|
136
|
+
(0, onboard_1.ensureGitignore)(cwd);
|
|
137
|
+
const validation = (0, generate_yaml_1.validateGeneratedConfig)(outputPath);
|
|
138
|
+
if (!validation.ok) {
|
|
139
|
+
throw new Error("Generated config failed validation:\n" + validation.errors.join("\n"));
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
async function runSetupDevinAndValidate(options) {
|
|
144
|
+
const result = await runSetupDevin(options);
|
|
145
|
+
const cwd = options.cwd ?? process.cwd();
|
|
146
|
+
const outputPath = node_path_1.default.resolve(cwd, options.outputPath ?? "docdrift.yaml");
|
|
147
|
+
if (outputPath === node_path_1.default.resolve(cwd, "docdrift.yaml")) {
|
|
148
|
+
try {
|
|
149
|
+
await (0, index_1.runValidate)();
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
@@ -32,22 +32,59 @@ function applyOverrides(base, overrides) {
|
|
|
32
32
|
function setByKey(obj, key, value) {
|
|
33
33
|
const parts = key.split(".");
|
|
34
34
|
let cur = obj;
|
|
35
|
-
|
|
35
|
+
let i = 0;
|
|
36
|
+
while (i < parts.length - 1) {
|
|
36
37
|
const p = parts[i];
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
const nextPart = parts[i + 1];
|
|
39
|
+
const isArrayIndex = nextPart !== undefined && /^\d+$/.test(nextPart);
|
|
40
|
+
const existing = cur[p];
|
|
41
|
+
if (existing != null && typeof existing === "object") {
|
|
42
|
+
if (Array.isArray(existing) && isArrayIndex) {
|
|
43
|
+
const idx = parseInt(nextPart, 10);
|
|
44
|
+
let el = existing[idx];
|
|
45
|
+
if (el == null || typeof el !== "object") {
|
|
46
|
+
el = {};
|
|
47
|
+
existing[idx] = el;
|
|
48
|
+
}
|
|
49
|
+
cur = el;
|
|
50
|
+
i += 2;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!Array.isArray(existing)) {
|
|
54
|
+
cur = existing;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
cur[p] = isArrayIndex ? [] : {};
|
|
58
|
+
cur = cur[p];
|
|
59
|
+
}
|
|
39
60
|
}
|
|
40
|
-
|
|
61
|
+
else {
|
|
62
|
+
const next = isArrayIndex ? [] : {};
|
|
63
|
+
cur[p] = next;
|
|
64
|
+
cur = next;
|
|
65
|
+
}
|
|
66
|
+
i++;
|
|
41
67
|
}
|
|
42
68
|
cur[parts[parts.length - 1]] = value;
|
|
43
69
|
}
|
|
44
70
|
const DEFAULT_CONFIG = {
|
|
45
|
-
version:
|
|
46
|
-
|
|
71
|
+
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
|
+
],
|
|
47
83
|
docsite: "apps/docs-site",
|
|
48
84
|
exclude: [],
|
|
49
85
|
requireHumanReview: [],
|
|
50
86
|
pathMappings: [],
|
|
87
|
+
mode: "strict",
|
|
51
88
|
devin: {
|
|
52
89
|
apiVersion: "v1",
|
|
53
90
|
unlisted: true,
|
package/dist/src/setup/index.js
CHANGED
|
@@ -1,109 +1,35 @@
|
|
|
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
|
-
})();
|
|
35
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
4
|
};
|
|
38
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
6
|
exports.runSetup = runSetup;
|
|
40
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
41
|
-
const
|
|
42
|
-
const ai_infer_1 = require("./ai-infer");
|
|
43
|
-
const interactive_form_1 = require("./interactive-form");
|
|
44
|
-
const generate_yaml_1 = require("./generate-yaml");
|
|
45
|
-
const onboard_1 = require("./onboard");
|
|
46
|
-
const index_1 = require("../index");
|
|
8
|
+
const devin_setup_1 = require("./devin-setup");
|
|
47
9
|
async function runSetup(options = {}) {
|
|
48
10
|
const cwd = options.cwd ?? process.cwd();
|
|
49
11
|
const outputPath = node_path_1.default.resolve(cwd, options.outputPath ?? "docdrift.yaml");
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
});
|
|
57
|
-
if (!overwrite) {
|
|
58
|
-
console.log("Setup cancelled.");
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
process.stdout.write("Analyzing your repo…\n");
|
|
63
|
-
const fingerprint = (0, repo_fingerprint_1.buildRepoFingerprint)(cwd);
|
|
64
|
-
process.stdout.write("Generating suggestions…\n");
|
|
65
|
-
const inference = await (0, ai_infer_1.inferConfigFromFingerprint)(fingerprint, cwd);
|
|
66
|
-
const formResult = await (0, interactive_form_1.runInteractiveForm)(inference, cwd);
|
|
67
|
-
let config = (0, generate_yaml_1.buildConfigFromInference)(inference, formResult);
|
|
68
|
-
if (formResult.onboarding.addCustomInstructions) {
|
|
69
|
-
const devin = config.devin ?? {};
|
|
70
|
-
config.devin = {
|
|
71
|
-
...devin,
|
|
72
|
-
customInstructions: [".docdrift/DocDrift.md"],
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
(0, generate_yaml_1.writeConfig)(config, outputPath);
|
|
76
|
-
const { created } = (0, onboard_1.runOnboarding)(cwd, formResult.onboarding);
|
|
77
|
-
const validation = (0, generate_yaml_1.validateGeneratedConfig)(outputPath);
|
|
78
|
-
if (!validation.ok) {
|
|
79
|
-
console.error("Config validation failed:\n" + validation.errors.join("\n"));
|
|
80
|
-
throw new Error("Generated config is invalid. Fix the errors above or edit docdrift.yaml manually.");
|
|
81
|
-
}
|
|
82
|
-
if (outputPath === node_path_1.default.resolve(cwd, "docdrift.yaml")) {
|
|
83
|
-
try {
|
|
84
|
-
await (0, index_1.runValidate)();
|
|
85
|
-
}
|
|
86
|
-
catch (err) {
|
|
87
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
88
|
-
throw err;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
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
|
+
});
|
|
91
18
|
console.log("\ndocdrift setup complete\n");
|
|
92
19
|
console.log(" docdrift.yaml written and validated");
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
else if (item === ".gitignore")
|
|
99
|
-
console.log(" .gitignore updated");
|
|
100
|
-
else if (item.endsWith("docdrift.yml"))
|
|
101
|
-
console.log(" " + item + " added");
|
|
20
|
+
if (result.docDriftMd)
|
|
21
|
+
console.log(" .docdrift/DocDrift.md created (edit for custom instructions)");
|
|
22
|
+
if (result.workflowYml) {
|
|
23
|
+
console.log(" .github/workflows/docdrift.yml added");
|
|
24
|
+
console.log(" .github/workflows/docdrift-sla-check.yml added");
|
|
102
25
|
}
|
|
26
|
+
console.log(" .gitignore updated");
|
|
27
|
+
console.log("\nSummary: " + result.summary);
|
|
28
|
+
console.log("\nSession: " + result.sessionUrl);
|
|
103
29
|
console.log("\nNext steps:");
|
|
104
|
-
console.log(" 1.
|
|
105
|
-
console.log(" 2.
|
|
106
|
-
console.log(" 3. Run: docdrift validate — verify config");
|
|
107
|
-
console.log(" 4. Run: docdrift detect — check for drift");
|
|
108
|
-
console.log(" 5. Run: docdrift run — create Devin session (requires DEVIN_API_KEY)");
|
|
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)");
|
|
109
35
|
}
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.ensureDocdriftDir = ensureDocdriftDir;
|
|
7
7
|
exports.createCustomInstructionsFile = createCustomInstructionsFile;
|
|
8
8
|
exports.ensureGitignore = ensureGitignore;
|
|
9
|
+
exports.addSlaCheckWorkflow = addSlaCheckWorkflow;
|
|
9
10
|
exports.addGitHubWorkflow = addGitHubWorkflow;
|
|
10
11
|
exports.runOnboarding = runOnboarding;
|
|
11
12
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -19,7 +20,129 @@ const GITIGNORE_BLOCK = `
|
|
|
19
20
|
.docdrift/state.json
|
|
20
21
|
.docdrift/run-output.json
|
|
21
22
|
`;
|
|
22
|
-
const WORKFLOW_CONTENT =
|
|
23
|
+
const WORKFLOW_CONTENT = `name: docdrift
|
|
24
|
+
|
|
25
|
+
on:
|
|
26
|
+
push:
|
|
27
|
+
branches: ["main"]
|
|
28
|
+
pull_request:
|
|
29
|
+
branches: ["main"]
|
|
30
|
+
workflow_dispatch:
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
docdrift:
|
|
34
|
+
if: github.event_name != 'pull_request' || !startsWith(github.event.pull_request.head.ref, 'docdrift/')
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
permissions:
|
|
37
|
+
contents: write
|
|
38
|
+
pull-requests: write
|
|
39
|
+
issues: write
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v4
|
|
42
|
+
with:
|
|
43
|
+
fetch-depth: 0
|
|
44
|
+
|
|
45
|
+
- uses: actions/setup-node@v4
|
|
46
|
+
with:
|
|
47
|
+
node-version: "20"
|
|
48
|
+
|
|
49
|
+
- run: npm install
|
|
50
|
+
|
|
51
|
+
- name: Determine SHAs
|
|
52
|
+
id: shas
|
|
53
|
+
run: |
|
|
54
|
+
if [ "\$\{\{ github.event_name \}\}" = "pull_request" ]; then
|
|
55
|
+
HEAD_SHA="\$\{\{ github.event.pull_request.head.sha \}\}"
|
|
56
|
+
BASE_SHA="\$\{\{ github.event.pull_request.base.sha \}\}"
|
|
57
|
+
else
|
|
58
|
+
HEAD_SHA="\$\{\{ github.sha \}\}"
|
|
59
|
+
BASE_SHA="\$\{\{ github.event.before \}\}"
|
|
60
|
+
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
|
61
|
+
BASE_SHA="$(git rev-parse HEAD^)"
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
echo "head=\${HEAD_SHA}" >> $GITHUB_OUTPUT
|
|
65
|
+
echo "base=\${BASE_SHA}" >> $GITHUB_OUTPUT
|
|
66
|
+
echo "pr_number=\$\{\{ github.event.pull_request.number || '' \}\}" >> $GITHUB_OUTPUT
|
|
67
|
+
|
|
68
|
+
- name: Restore docdrift state
|
|
69
|
+
uses: actions/cache/restore@v4
|
|
70
|
+
id: docdrift-cache
|
|
71
|
+
with:
|
|
72
|
+
path: .docdrift
|
|
73
|
+
key: docdrift-state-\$\{\{ github.event_name \}\}-\$\{\{ github.event.pull_request.number || 'main' \}\}-\$\{\{ github.run_id \}\}
|
|
74
|
+
restore-keys: |
|
|
75
|
+
docdrift-state-\$\{\{ github.event_name \}\}-\$\{\{ github.event.pull_request.number || 'main' \}\}-
|
|
76
|
+
|
|
77
|
+
- name: Validate config
|
|
78
|
+
run: npx @devinnn/docdrift validate
|
|
79
|
+
|
|
80
|
+
- name: Run Doc Drift
|
|
81
|
+
env:
|
|
82
|
+
DEVIN_API_KEY: \$\{\{ secrets.DEVIN_API_KEY \}\}
|
|
83
|
+
GITHUB_TOKEN: \$\{\{ secrets.GITHUB_TOKEN \}\}
|
|
84
|
+
GITHUB_REPOSITORY: \$\{\{ github.repository \}\}
|
|
85
|
+
GITHUB_SHA: \$\{\{ github.sha \}\}
|
|
86
|
+
GITHUB_EVENT_NAME: \$\{\{ github.event_name \}\}
|
|
87
|
+
GITHUB_PR_NUMBER: \$\{\{ steps.shas.outputs.pr_number \}\}
|
|
88
|
+
run: |
|
|
89
|
+
PR_ARGS=""
|
|
90
|
+
if [ -n "$GITHUB_PR_NUMBER" ]; then
|
|
91
|
+
PR_ARGS="--trigger pull_request --pr-number $GITHUB_PR_NUMBER"
|
|
92
|
+
fi
|
|
93
|
+
npx @devinnn/docdrift run --base \$\{\{ steps.shas.outputs.base \}\} --head \$\{\{ steps.shas.outputs.head \}\} $PR_ARGS
|
|
94
|
+
|
|
95
|
+
- name: Save docdrift state
|
|
96
|
+
if: always()
|
|
97
|
+
uses: actions/cache/save@v4
|
|
98
|
+
with:
|
|
99
|
+
path: .docdrift
|
|
100
|
+
key: docdrift-state-\$\{\{ github.event_name \}\}-\$\{\{ github.event.pull_request.number || 'main' \}\}-\$\{\{ github.run_id \}\}
|
|
101
|
+
|
|
102
|
+
- name: Upload artifacts
|
|
103
|
+
if: always()
|
|
104
|
+
uses: actions/upload-artifact@v4
|
|
105
|
+
with:
|
|
106
|
+
name: docdrift-artifacts
|
|
107
|
+
path: |
|
|
108
|
+
.docdrift/drift_report.json
|
|
109
|
+
.docdrift/metrics.json
|
|
110
|
+
.docdrift/run-output.json
|
|
111
|
+
.docdrift/evidence/**
|
|
112
|
+
.docdrift/state.json
|
|
113
|
+
`;
|
|
114
|
+
const SLA_CHECK_WORKFLOW_CONTENT = `name: docdrift-sla-check
|
|
115
|
+
|
|
116
|
+
on:
|
|
117
|
+
schedule:
|
|
118
|
+
# Run daily at 09:00 UTC (checks for doc-drift PRs open 7+ days)
|
|
119
|
+
- cron: "0 9 * * *"
|
|
120
|
+
workflow_dispatch:
|
|
121
|
+
|
|
122
|
+
jobs:
|
|
123
|
+
sla-check:
|
|
124
|
+
runs-on: ubuntu-latest
|
|
125
|
+
permissions:
|
|
126
|
+
contents: read
|
|
127
|
+
issues: write
|
|
128
|
+
steps:
|
|
129
|
+
- uses: actions/checkout@v4
|
|
130
|
+
|
|
131
|
+
- uses: actions/setup-node@v4
|
|
132
|
+
with:
|
|
133
|
+
node-version: "20"
|
|
134
|
+
|
|
135
|
+
- run: npm install
|
|
136
|
+
|
|
137
|
+
- name: Validate config
|
|
138
|
+
run: npx @devinnn/docdrift validate
|
|
139
|
+
|
|
140
|
+
- name: Run SLA check
|
|
141
|
+
env:
|
|
142
|
+
GITHUB_TOKEN: \$\{\{ secrets.GITHUB_TOKEN \}\}
|
|
143
|
+
GITHUB_REPOSITORY: \$\{\{ github.repository \}\}
|
|
144
|
+
run: npx @devinnn/docdrift sla-check
|
|
145
|
+
`;
|
|
23
146
|
function ensureDocdriftDir(cwd) {
|
|
24
147
|
const dir = node_path_1.default.resolve(cwd, DOCDRIFT_DIR);
|
|
25
148
|
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
@@ -54,11 +177,16 @@ function ensureGitignore(cwd) {
|
|
|
54
177
|
const toAppend = content.endsWith("\n") ? GITIGNORE_BLOCK.trimStart() : GITIGNORE_BLOCK;
|
|
55
178
|
node_fs_1.default.writeFileSync(gitignorePath, content + toAppend, "utf8");
|
|
56
179
|
}
|
|
180
|
+
function addSlaCheckWorkflow(cwd) {
|
|
181
|
+
const workflowsDir = node_path_1.default.resolve(cwd, ".github", "workflows");
|
|
182
|
+
node_fs_1.default.mkdirSync(workflowsDir, { recursive: true });
|
|
183
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(workflowsDir, "docdrift-sla-check.yml"), SLA_CHECK_WORKFLOW_CONTENT, "utf8");
|
|
184
|
+
}
|
|
57
185
|
function addGitHubWorkflow(cwd) {
|
|
58
186
|
const workflowsDir = node_path_1.default.resolve(cwd, ".github", "workflows");
|
|
59
187
|
node_fs_1.default.mkdirSync(workflowsDir, { recursive: true });
|
|
60
|
-
|
|
61
|
-
|
|
188
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(workflowsDir, "docdrift.yml"), WORKFLOW_CONTENT, "utf8");
|
|
189
|
+
addSlaCheckWorkflow(cwd);
|
|
62
190
|
}
|
|
63
191
|
function runOnboarding(cwd, choices) {
|
|
64
192
|
const created = [];
|
|
@@ -75,6 +203,7 @@ function runOnboarding(cwd, choices) {
|
|
|
75
203
|
if (choices.addWorkflow) {
|
|
76
204
|
addGitHubWorkflow(cwd);
|
|
77
205
|
created.push(".github/workflows/docdrift.yml");
|
|
206
|
+
created.push(".github/workflows/docdrift-sla-check.yml");
|
|
78
207
|
}
|
|
79
208
|
return { created };
|
|
80
209
|
}
|
|
@@ -3,23 +3,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SYSTEM_PROMPT = void 0;
|
|
4
4
|
exports.SYSTEM_PROMPT = `You are a docdrift config expert. Given a repo fingerprint (file tree, package.json scripts, and detected paths), infer a partial docdrift.yaml configuration and a list of interactive choices for the user.
|
|
5
5
|
|
|
6
|
-
## Docdrift config (
|
|
6
|
+
## Docdrift config (v2)
|
|
7
7
|
|
|
8
|
-
Minimal valid config uses: version,
|
|
8
|
+
Minimal valid config uses: version: 2, specProviders (or pathMappings only for path-only setups), docsite, devin, policy.
|
|
9
9
|
|
|
10
10
|
Example:
|
|
11
11
|
\`\`\`yaml
|
|
12
|
-
version:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
version: 2
|
|
13
|
+
specProviders:
|
|
14
|
+
- format: openapi3
|
|
15
|
+
current:
|
|
16
|
+
type: export
|
|
17
|
+
command: "npm run openapi:export"
|
|
18
|
+
outputPath: "openapi/generated.json"
|
|
19
|
+
published: "apps/docs-site/openapi/openapi.json"
|
|
17
20
|
docsite: "apps/docs-site"
|
|
18
21
|
pathMappings:
|
|
19
22
|
- match: "apps/api/**"
|
|
20
23
|
impacts: ["apps/docs-site/docs/**", "apps/docs-site/openapi/**"]
|
|
21
24
|
exclude: ["**/CHANGELOG*", "apps/docs-site/blog/**"]
|
|
22
25
|
requireHumanReview: []
|
|
26
|
+
mode: strict
|
|
23
27
|
devin:
|
|
24
28
|
apiVersion: v1
|
|
25
29
|
unlisted: true
|
|
@@ -38,25 +42,88 @@ policy:
|
|
|
38
42
|
|
|
39
43
|
## Field rules
|
|
40
44
|
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
- openapi.published: Path where the docsite consumes the spec (often under docsite, e.g. "apps/docs-site/openapi/openapi.json").
|
|
45
|
+
- 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>".
|
|
44
47
|
- docsite: Path to the docs site root (Docusaurus, Next.js docs, VitePress, MkDocs). Single string or array of strings.
|
|
45
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.
|
|
46
50
|
- policy.verification.commands: Commands to run after patching (e.g. "npm run docs:gen", "npm run docs:build"). Must exist in repo.
|
|
47
51
|
- exclude: Globs to never touch (e.g. blog, CHANGELOG).
|
|
48
52
|
- requireHumanReview: Globs that require human review when touched (e.g. guides).
|
|
49
53
|
|
|
54
|
+
## Path-only config (no OpenAPI)
|
|
55
|
+
|
|
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
|
+
\`\`\`
|
|
63
|
+
|
|
50
64
|
## Common patterns
|
|
51
65
|
|
|
52
|
-
- Docusaurus: docsite often has docusaurus.config.*; docs:gen may be "docusaurus -- gen-api-docs api";
|
|
66
|
+
- Docusaurus: docsite often has docusaurus.config.*; docs:gen may be "docusaurus -- gen-api-docs api"; published path often under docsite/openapi/.
|
|
53
67
|
- Next/VitePress/MkDocs: docsite is the app root; look for docs/ or similar.
|
|
54
68
|
|
|
55
69
|
## Output rules
|
|
56
70
|
|
|
57
|
-
1. Infer suggestedConfig from the fingerprint. Only include fields you can confidently infer. Use existing paths and scripts from the fingerprint; do not invent paths that are not present.
|
|
58
|
-
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. "
|
|
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").
|
|
59
73
|
3. Add to skipQuestions the keys for which you are highly confident so the CLI will not ask the user.
|
|
60
74
|
4. Prefer fewer, high-quality choices. If truly uncertain, set confidence to "low" and provide 2–3 options.
|
|
61
75
|
5. Do not suggest paths that do not exist in the fingerprint. Prefer existing package.json scripts for export and verification commands.
|
|
62
|
-
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
|
|
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
|
|
129
|
+
`;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Prompt for Devin setup session — Devin analyzes the repo and generates
|
|
4
|
+
* docdrift.yaml, DocDrift.md, and GitHub workflow. The repo is already in
|
|
5
|
+
* Devin's Machine, so Devin has full context.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.buildSetupPrompt = buildSetupPrompt;
|
|
9
|
+
function attachmentBlock(urls) {
|
|
10
|
+
return urls.map((url, i) => `- ATTACHMENT ${i + 1}: ${url}`).join("\n");
|
|
11
|
+
}
|
|
12
|
+
function buildSetupPrompt(attachmentUrls) {
|
|
13
|
+
return [
|
|
14
|
+
"You are Devin. Task: set up docdrift for this repository.",
|
|
15
|
+
"",
|
|
16
|
+
"This repo is already loaded in your environment. Analyze it and produce the docdrift configuration files.",
|
|
17
|
+
"",
|
|
18
|
+
"ATTACHMENTS (read these for spec and schema):",
|
|
19
|
+
attachmentBlock(attachmentUrls),
|
|
20
|
+
"",
|
|
21
|
+
"REQUIREMENTS:",
|
|
22
|
+
"",
|
|
23
|
+
"1) docdrift.yaml (REQUIRED)",
|
|
24
|
+
" - Use version: 2",
|
|
25
|
+
" - Use specProviders format with format: openapi3",
|
|
26
|
+
" - Infer: current (type: export, command, outputPath), published path",
|
|
27
|
+
" - Set docsite to your docs root (e.g. docs, apps/docs-site)",
|
|
28
|
+
" - devin: apiVersion v1, unlisted true, maxAcuLimit 2, tags [docdrift]",
|
|
29
|
+
" - If you find an OpenAPI/swagger file or export script, use it",
|
|
30
|
+
" - policy: prCaps, confidence, allowlist (paths Devin may edit), verification.commands (e.g. npm run docs:build, npm run build)",
|
|
31
|
+
" - Add schema comment at top: # yaml-language-server: $schema=https://unpkg.com/@devinnn/docdrift/docdrift.schema.json",
|
|
32
|
+
"",
|
|
33
|
+
"2) .docdrift/DocDrift.md (RECOMMENDED)",
|
|
34
|
+
" - Starter custom instructions: PR title prefix [docdrift], tone, project-specific guidance",
|
|
35
|
+
" - If you include this, set devin.customInstructions: [.docdrift/DocDrift.md] in docdrift.yaml",
|
|
36
|
+
"",
|
|
37
|
+
"3) .github/workflows/docdrift.yml (RECOMMENDED)",
|
|
38
|
+
" - CRITICAL: Use npx @devinnn/docdrift (not npx docdrift)",
|
|
39
|
+
" - Steps: checkout, setup-node 20, Determine SHAs (base/head for push or PR), Validate config, Run Doc Drift",
|
|
40
|
+
" - Env: DEVIN_API_KEY, GITHUB_TOKEN, GITHUB_REPOSITORY, GITHUB_SHA",
|
|
41
|
+
" - Skip when PR head ref starts with docdrift/ (avoid feedback loop)",
|
|
42
|
+
" - Upload .docdrift artifacts (drift_report.json, metrics.json, evidence, state.json)",
|
|
43
|
+
" - Note: docdrift-sla-check.yml (daily cron for PRs open 7+ days) is added automatically",
|
|
44
|
+
"",
|
|
45
|
+
"OUTPUT:",
|
|
46
|
+
"Emit your final output in the provided structured output schema.",
|
|
47
|
+
"- docdriftYaml: complete YAML string (no leading/trailing comments about the task)",
|
|
48
|
+
"- docDriftMd: content for .docdrift/DocDrift.md, or empty string to omit",
|
|
49
|
+
"- workflowYml: content for .github/workflows/docdrift.yml, or empty string to omit",
|
|
50
|
+
"- summary: what you inferred (openapi export, docsite path, verification commands)",
|
|
51
|
+
"",
|
|
52
|
+
"Do NOT create files in the repo. Only produce the structured output.",
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
package/docdrift.schema.json
CHANGED
|
@@ -183,13 +183,13 @@
|
|
|
183
183
|
},
|
|
184
184
|
"default": []
|
|
185
185
|
},
|
|
186
|
-
"
|
|
187
|
-
"type": "
|
|
188
|
-
"
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
"default":
|
|
186
|
+
"mode": {
|
|
187
|
+
"type": "string",
|
|
188
|
+
"enum": [
|
|
189
|
+
"strict",
|
|
190
|
+
"auto"
|
|
191
|
+
],
|
|
192
|
+
"default": "strict"
|
|
193
193
|
},
|
|
194
194
|
"devin": {
|
|
195
195
|
"type": "object",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devinnn/docdrift",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Detect and remediate documentation drift with Devin sessions",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -61,4 +61,4 @@
|
|
|
61
61
|
"vitest": "^3.0.5",
|
|
62
62
|
"zod-to-json-schema": "^3.25.1"
|
|
63
63
|
}
|
|
64
|
-
}
|
|
64
|
+
}
|