@devinnn/docdrift 0.1.2 → 0.1.4
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/dist/src/cli.js +53 -9
- package/dist/src/config/normalize.js +93 -6
- package/dist/src/config/schema.js +78 -18
- package/dist/src/config/validate.js +6 -2
- package/dist/src/detect/index.js +76 -21
- package/dist/src/devin/prompts.js +45 -1
- package/dist/src/devin/v1.js +26 -1
- package/dist/src/github/client.js +13 -0
- package/dist/src/index.js +71 -11
- package/dist/src/setup/ai-infer.js +191 -0
- package/dist/src/setup/generate-yaml.js +95 -0
- package/dist/src/setup/index.js +109 -0
- package/dist/src/setup/interactive-form.js +49 -0
- package/dist/src/setup/onboard.js +80 -0
- package/dist/src/setup/prompts.js +62 -0
- package/dist/src/setup/repo-fingerprint.js +155 -0
- package/dist/src/spec-providers/fern.js +123 -0
- package/dist/src/spec-providers/graphql.js +168 -0
- package/dist/src/spec-providers/openapi.js +181 -0
- package/dist/src/spec-providers/postman.js +193 -0
- package/dist/src/spec-providers/registry.js +26 -0
- package/dist/src/spec-providers/swagger2.js +229 -0
- package/dist/src/spec-providers/types.js +2 -0
- package/dist/src/utils/fetch.js +87 -0
- package/dist/src/utils/git.js +20 -0
- package/docdrift.schema.json +438 -0
- package/package.json +14 -5
package/dist/src/index.js
CHANGED
|
@@ -1,9 +1,42 @@
|
|
|
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
|
-
exports.STATE_PATH = void 0;
|
|
39
|
+
exports.runSetup = exports.STATE_PATH = void 0;
|
|
7
40
|
exports.runDetect = runDetect;
|
|
8
41
|
exports.runDocDrift = runDocDrift;
|
|
9
42
|
exports.runValidate = runValidate;
|
|
@@ -12,6 +45,7 @@ exports.runStatus = runStatus;
|
|
|
12
45
|
exports.resolveTrigger = resolveTrigger;
|
|
13
46
|
exports.parseDurationHours = parseDurationHours;
|
|
14
47
|
exports.requireSha = requireSha;
|
|
48
|
+
exports.resolveBaseHead = resolveBaseHead;
|
|
15
49
|
const node_path_1 = __importDefault(require("node:path"));
|
|
16
50
|
const load_1 = require("./config/load");
|
|
17
51
|
const validate_1 = require("./config/validate");
|
|
@@ -60,6 +94,9 @@ async function executeSessionSingle(input) {
|
|
|
60
94
|
aggregated: input.aggregated,
|
|
61
95
|
config: input.config,
|
|
62
96
|
attachmentUrls,
|
|
97
|
+
runGate: input.runGate,
|
|
98
|
+
trigger: input.trigger,
|
|
99
|
+
prNumber: input.prNumber,
|
|
63
100
|
});
|
|
64
101
|
const session = await (0, v1_1.devinCreateSession)(input.apiKey, {
|
|
65
102
|
prompt,
|
|
@@ -123,14 +160,15 @@ async function runDetect(options) {
|
|
|
123
160
|
}
|
|
124
161
|
const repo = process.env.GITHUB_REPOSITORY ?? "local/docdrift";
|
|
125
162
|
const normalized = (0, load_1.loadNormalizedConfig)();
|
|
126
|
-
const { report,
|
|
163
|
+
const { report, runGate } = await (0, detect_1.buildDriftReport)({
|
|
127
164
|
config: normalized,
|
|
128
165
|
repo,
|
|
129
166
|
baseSha: options.baseSha,
|
|
130
167
|
headSha: options.headSha,
|
|
131
168
|
trigger: options.trigger ?? "manual",
|
|
169
|
+
prNumber: options.prNumber,
|
|
132
170
|
});
|
|
133
|
-
(0, log_1.logInfo)(`Drift items detected: ${report.items.length} (
|
|
171
|
+
(0, log_1.logInfo)(`Drift items detected: ${report.items.length} (runGate: ${runGate})`);
|
|
134
172
|
return { hasDrift: report.items.length > 0 };
|
|
135
173
|
}
|
|
136
174
|
async function runDocDrift(options) {
|
|
@@ -144,16 +182,17 @@ async function runDocDrift(options) {
|
|
|
144
182
|
const commitSha = process.env.GITHUB_SHA ?? options.headSha;
|
|
145
183
|
const githubToken = process.env.GITHUB_TOKEN;
|
|
146
184
|
const devinApiKey = process.env.DEVIN_API_KEY;
|
|
147
|
-
const { report, aggregated, runInfo, evidenceRoot,
|
|
185
|
+
const { report, aggregated, runInfo, evidenceRoot, runGate } = await (0, detect_1.buildDriftReport)({
|
|
148
186
|
config: normalized,
|
|
149
187
|
repo,
|
|
150
188
|
baseSha: options.baseSha,
|
|
151
189
|
headSha: options.headSha,
|
|
152
190
|
trigger: options.trigger ?? "manual",
|
|
191
|
+
prNumber: options.prNumber,
|
|
153
192
|
});
|
|
154
|
-
// Gate: no
|
|
155
|
-
if (
|
|
156
|
-
(0, log_1.logInfo)("No
|
|
193
|
+
// Gate: no run (spec drift, conceptual-only, or infer) — exit early, no session
|
|
194
|
+
if (runGate === "none" || report.items.length === 0) {
|
|
195
|
+
(0, log_1.logInfo)("No drift; skipping session");
|
|
157
196
|
return [];
|
|
158
197
|
}
|
|
159
198
|
const item = report.items[0];
|
|
@@ -234,6 +273,9 @@ async function runDocDrift(options) {
|
|
|
234
273
|
aggregated: aggregated,
|
|
235
274
|
attachmentPaths,
|
|
236
275
|
config: normalized,
|
|
276
|
+
runGate,
|
|
277
|
+
trigger: runInfo.trigger,
|
|
278
|
+
prNumber: runInfo.prNumber,
|
|
237
279
|
});
|
|
238
280
|
metrics.timeToSessionTerminalMs.push(Date.now() - sessionStart);
|
|
239
281
|
}
|
|
@@ -254,6 +296,14 @@ async function runDocDrift(options) {
|
|
|
254
296
|
metrics.prsOpened += 1;
|
|
255
297
|
state.lastDocDriftPrUrl = sessionOutcome.prUrl;
|
|
256
298
|
state.lastDocDriftPrOpenedAt = new Date().toISOString();
|
|
299
|
+
if (githubToken && runInfo.trigger === "pull_request" && runInfo.prNumber) {
|
|
300
|
+
await (0, client_1.postPrComment)({
|
|
301
|
+
token: githubToken,
|
|
302
|
+
repository: repo,
|
|
303
|
+
prNumber: runInfo.prNumber,
|
|
304
|
+
body: `## Doc drift detected\n\nDraft doc PR: ${sessionOutcome.prUrl}\n\nMerge your API changes first, then review and merge this doc PR.`,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
257
307
|
const touchedRequireReview = (item.impactedDocs ?? []).filter((p) => normalized.requireHumanReview.some((glob) => (0, glob_1.matchesGlob)(glob, p)));
|
|
258
308
|
if (githubToken && touchedRequireReview.length > 0) {
|
|
259
309
|
issueUrl = await (0, client_1.createIssue)({
|
|
@@ -450,12 +500,12 @@ async function runStatus(sinceHours = 24) {
|
|
|
450
500
|
}
|
|
451
501
|
}
|
|
452
502
|
function resolveTrigger(eventName) {
|
|
453
|
-
if (eventName === "push")
|
|
503
|
+
if (eventName === "push")
|
|
454
504
|
return "push";
|
|
455
|
-
|
|
456
|
-
if (eventName === "schedule") {
|
|
505
|
+
if (eventName === "schedule")
|
|
457
506
|
return "schedule";
|
|
458
|
-
|
|
507
|
+
if (eventName === "pull_request")
|
|
508
|
+
return "pull_request";
|
|
459
509
|
return "manual";
|
|
460
510
|
}
|
|
461
511
|
function parseDurationHours(value) {
|
|
@@ -475,4 +525,14 @@ function requireSha(value, label) {
|
|
|
475
525
|
}
|
|
476
526
|
return value;
|
|
477
527
|
}
|
|
528
|
+
async function resolveBaseHead(baseArg, headArg) {
|
|
529
|
+
const headRef = headArg ?? process.env.GITHUB_SHA ?? "HEAD";
|
|
530
|
+
if (baseArg) {
|
|
531
|
+
return { baseSha: baseArg, headSha: headRef };
|
|
532
|
+
}
|
|
533
|
+
const { resolveDefaultBaseHead } = await Promise.resolve().then(() => __importStar(require("./utils/git")));
|
|
534
|
+
return resolveDefaultBaseHead(headRef);
|
|
535
|
+
}
|
|
478
536
|
exports.STATE_PATH = node_path_1.default.resolve(".docdrift", "state.json");
|
|
537
|
+
var setup_1 = require("./setup");
|
|
538
|
+
Object.defineProperty(exports, "runSetup", { enumerable: true, get: function () { return setup_1.runSetup; } });
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.inferConfigFromFingerprint = inferConfigFromFingerprint;
|
|
7
|
+
const gateway_1 = require("@ai-sdk/gateway");
|
|
8
|
+
const ai_1 = require("ai");
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const zod_1 = require("zod");
|
|
12
|
+
const repo_fingerprint_1 = require("./repo-fingerprint");
|
|
13
|
+
const prompts_1 = require("./prompts");
|
|
14
|
+
const pathRuleSchema = zod_1.z.object({
|
|
15
|
+
match: zod_1.z.string().min(1),
|
|
16
|
+
impacts: zod_1.z.array(zod_1.z.string().min(1)).min(1),
|
|
17
|
+
});
|
|
18
|
+
const InferenceSchema = zod_1.z.object({
|
|
19
|
+
suggestedConfig: zod_1.z.object({
|
|
20
|
+
version: zod_1.z.union([zod_1.z.literal(1), zod_1.z.literal(2)]).optional(),
|
|
21
|
+
openapi: zod_1.z
|
|
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(),
|
|
28
|
+
docsite: zod_1.z.union([zod_1.z.string().min(1), zod_1.z.array(zod_1.z.string().min(1))]).optional(),
|
|
29
|
+
exclude: zod_1.z.array(zod_1.z.string().min(1)).optional(),
|
|
30
|
+
requireHumanReview: zod_1.z.array(zod_1.z.string().min(1)).optional(),
|
|
31
|
+
pathMappings: zod_1.z.array(pathRuleSchema).optional(),
|
|
32
|
+
devin: zod_1.z
|
|
33
|
+
.object({
|
|
34
|
+
apiVersion: zod_1.z.literal("v1"),
|
|
35
|
+
unlisted: zod_1.z.boolean().optional(),
|
|
36
|
+
maxAcuLimit: zod_1.z.number().optional(),
|
|
37
|
+
tags: zod_1.z.array(zod_1.z.string()).optional(),
|
|
38
|
+
customInstructions: zod_1.z.array(zod_1.z.string()).optional(),
|
|
39
|
+
})
|
|
40
|
+
.optional(),
|
|
41
|
+
policy: zod_1.z
|
|
42
|
+
.object({
|
|
43
|
+
prCaps: zod_1.z.object({ maxPrsPerDay: zod_1.z.number(), maxFilesTouched: zod_1.z.number() }).optional(),
|
|
44
|
+
confidence: zod_1.z.object({ autopatchThreshold: zod_1.z.number() }).optional(),
|
|
45
|
+
allowlist: zod_1.z.array(zod_1.z.string().min(1)).optional(),
|
|
46
|
+
verification: zod_1.z.object({ commands: zod_1.z.array(zod_1.z.string().min(1)) }).optional(),
|
|
47
|
+
slaDays: zod_1.z.number().optional(),
|
|
48
|
+
slaLabel: zod_1.z.string().optional(),
|
|
49
|
+
allowNewFiles: zod_1.z.boolean().optional(),
|
|
50
|
+
})
|
|
51
|
+
.optional(),
|
|
52
|
+
}),
|
|
53
|
+
choices: zod_1.z.array(zod_1.z.object({
|
|
54
|
+
key: zod_1.z.string(),
|
|
55
|
+
question: zod_1.z.string(),
|
|
56
|
+
options: zod_1.z.array(zod_1.z.object({
|
|
57
|
+
value: zod_1.z.string(),
|
|
58
|
+
label: zod_1.z.string(),
|
|
59
|
+
recommended: zod_1.z.boolean().optional(),
|
|
60
|
+
})),
|
|
61
|
+
defaultIndex: zod_1.z.number(),
|
|
62
|
+
help: zod_1.z.string().optional(),
|
|
63
|
+
warning: zod_1.z.string().optional(),
|
|
64
|
+
confidence: zod_1.z.enum(["high", "medium", "low"]),
|
|
65
|
+
})),
|
|
66
|
+
skipQuestions: zod_1.z.array(zod_1.z.string()).optional(),
|
|
67
|
+
});
|
|
68
|
+
const CACHE_DIR = ".docdrift";
|
|
69
|
+
const CACHE_FILE = "setup-cache.json";
|
|
70
|
+
function getCachePath(cwd) {
|
|
71
|
+
return node_path_1.default.resolve(cwd, CACHE_DIR, CACHE_FILE);
|
|
72
|
+
}
|
|
73
|
+
function readCache(cwd) {
|
|
74
|
+
const cachePath = getCachePath(cwd);
|
|
75
|
+
if (!node_fs_1.default.existsSync(cachePath))
|
|
76
|
+
return null;
|
|
77
|
+
try {
|
|
78
|
+
const raw = JSON.parse(node_fs_1.default.readFileSync(cachePath, "utf8"));
|
|
79
|
+
const parsed = InferenceSchema.safeParse(raw.inference);
|
|
80
|
+
if (!parsed.success)
|
|
81
|
+
return null;
|
|
82
|
+
return {
|
|
83
|
+
fingerprintHash: String(raw.fingerprintHash),
|
|
84
|
+
inference: parsed.data,
|
|
85
|
+
timestamp: Number(raw.timestamp) || 0,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function writeCache(cwd, fingerprintHash, inference) {
|
|
93
|
+
const dir = node_path_1.default.resolve(cwd, CACHE_DIR);
|
|
94
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
95
|
+
node_fs_1.default.writeFileSync(getCachePath(cwd), JSON.stringify({ fingerprintHash, inference, timestamp: Date.now() }, null, 2), "utf8");
|
|
96
|
+
}
|
|
97
|
+
function heuristicInference(fingerprint) {
|
|
98
|
+
const scripts = fingerprint.rootPackage.scripts || {};
|
|
99
|
+
const openapiExport = scripts["openapi:export"] ?? scripts["openapi:generate"] ?? "npm run openapi:export";
|
|
100
|
+
const firstOpenapi = fingerprint.foundPaths.openapi[0];
|
|
101
|
+
const firstDocsite = fingerprint.foundPaths.docusaurusConfig[0]
|
|
102
|
+
? node_path_1.default.dirname(fingerprint.foundPaths.docusaurusConfig[0]).replace(/\\/g, "/")
|
|
103
|
+
: fingerprint.foundPaths.docsDirs[0]
|
|
104
|
+
? node_path_1.default.dirname(fingerprint.foundPaths.docsDirs[0]).replace(/\\/g, "/")
|
|
105
|
+
: "apps/docs-site";
|
|
106
|
+
const published = firstOpenapi ?? `${firstDocsite}/openapi/openapi.json`;
|
|
107
|
+
const generated = firstOpenapi ?? "openapi/generated.json";
|
|
108
|
+
const verificationCommands = [];
|
|
109
|
+
if (scripts["docs:gen"])
|
|
110
|
+
verificationCommands.push("npm run docs:gen");
|
|
111
|
+
if (scripts["docs:build"])
|
|
112
|
+
verificationCommands.push("npm run docs:build");
|
|
113
|
+
if (verificationCommands.length === 0)
|
|
114
|
+
verificationCommands.push("npm run build");
|
|
115
|
+
return {
|
|
116
|
+
suggestedConfig: {
|
|
117
|
+
version: 1,
|
|
118
|
+
openapi: { export: openapiExport, generated, published },
|
|
119
|
+
docsite: firstDocsite,
|
|
120
|
+
exclude: ["**/CHANGELOG*", "**/blog/**"],
|
|
121
|
+
requireHumanReview: [],
|
|
122
|
+
pathMappings: [{ match: "**/api/**", impacts: [`${firstDocsite}/docs/**`, `${firstDocsite}/openapi/**`] }],
|
|
123
|
+
devin: { apiVersion: "v1", unlisted: true, maxAcuLimit: 2, tags: ["docdrift"] },
|
|
124
|
+
policy: {
|
|
125
|
+
prCaps: { maxPrsPerDay: 5, maxFilesTouched: 30 },
|
|
126
|
+
confidence: { autopatchThreshold: 0.8 },
|
|
127
|
+
allowlist: ["openapi/**", firstDocsite + "/**"],
|
|
128
|
+
verification: { commands: verificationCommands },
|
|
129
|
+
slaDays: 7,
|
|
130
|
+
slaLabel: "docdrift",
|
|
131
|
+
allowNewFiles: false,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
choices: [
|
|
135
|
+
{
|
|
136
|
+
key: "openapi.export",
|
|
137
|
+
question: "OpenAPI export command",
|
|
138
|
+
options: [{ value: openapiExport, label: openapiExport, recommended: true }],
|
|
139
|
+
defaultIndex: 0,
|
|
140
|
+
help: "Command that generates the OpenAPI spec.",
|
|
141
|
+
confidence: "medium",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
key: "docsite",
|
|
145
|
+
question: "Docsite path",
|
|
146
|
+
options: [{ value: firstDocsite, label: firstDocsite, recommended: true }],
|
|
147
|
+
defaultIndex: 0,
|
|
148
|
+
confidence: "medium",
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
skipQuestions: [],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async function inferConfigFromFingerprint(fingerprint, cwd = process.cwd()) {
|
|
155
|
+
const apiKey = process.env.AI_GATEWAY_API_KEY?.trim();
|
|
156
|
+
const hash = (0, repo_fingerprint_1.fingerprintHash)(fingerprint);
|
|
157
|
+
const cached = readCache(cwd);
|
|
158
|
+
if (cached && cached.fingerprintHash === hash)
|
|
159
|
+
return cached.inference;
|
|
160
|
+
if (!apiKey)
|
|
161
|
+
return heuristicInference(fingerprint);
|
|
162
|
+
const gateway = (0, gateway_1.createGateway)({
|
|
163
|
+
apiKey,
|
|
164
|
+
baseURL: "https://ai-gateway.vercel.sh/v1/ai",
|
|
165
|
+
});
|
|
166
|
+
const prompt = `Repo fingerprint:\n${JSON.stringify(fingerprint, null, 2)}`;
|
|
167
|
+
try {
|
|
168
|
+
const result = await (0, ai_1.generateText)({
|
|
169
|
+
model: gateway("anthropic/claude-opus-4.6"),
|
|
170
|
+
system: prompts_1.SYSTEM_PROMPT,
|
|
171
|
+
prompt,
|
|
172
|
+
experimental_output: ai_1.Output.object({
|
|
173
|
+
schema: InferenceSchema,
|
|
174
|
+
}),
|
|
175
|
+
maxRetries: 2,
|
|
176
|
+
abortSignal: AbortSignal.timeout(60_000),
|
|
177
|
+
});
|
|
178
|
+
const output = result.experimental_output;
|
|
179
|
+
if (!output)
|
|
180
|
+
throw new Error("No structured output");
|
|
181
|
+
const parsed = InferenceSchema.safeParse(output);
|
|
182
|
+
if (!parsed.success)
|
|
183
|
+
throw new Error(parsed.error.message);
|
|
184
|
+
const inference = parsed.data;
|
|
185
|
+
writeCache(cwd, hash, inference);
|
|
186
|
+
return inference;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return heuristicInference(fingerprint);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildConfigFromInference = buildConfigFromInference;
|
|
7
|
+
exports.writeConfig = writeConfig;
|
|
8
|
+
exports.validateGeneratedConfig = validateGeneratedConfig;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
12
|
+
const schema_1 = require("../config/schema");
|
|
13
|
+
function deepMerge(target, source) {
|
|
14
|
+
const out = { ...target };
|
|
15
|
+
for (const key of Object.keys(source)) {
|
|
16
|
+
const s = source[key];
|
|
17
|
+
const t = out[key];
|
|
18
|
+
if (s != null && typeof s === "object" && !Array.isArray(s) && t != null && typeof t === "object" && !Array.isArray(t)) {
|
|
19
|
+
out[key] = deepMerge(t, s);
|
|
20
|
+
}
|
|
21
|
+
else if (s !== undefined) {
|
|
22
|
+
out[key] = s;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
function applyOverrides(base, overrides) {
|
|
28
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
29
|
+
setByKey(base, key, value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function setByKey(obj, key, value) {
|
|
33
|
+
const parts = key.split(".");
|
|
34
|
+
let cur = obj;
|
|
35
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
36
|
+
const p = parts[i];
|
|
37
|
+
if (!(p in cur) || typeof cur[p] !== "object" || cur[p] === null || Array.isArray(cur[p])) {
|
|
38
|
+
cur[p] = {};
|
|
39
|
+
}
|
|
40
|
+
cur = cur[p];
|
|
41
|
+
}
|
|
42
|
+
cur[parts[parts.length - 1]] = value;
|
|
43
|
+
}
|
|
44
|
+
const DEFAULT_CONFIG = {
|
|
45
|
+
version: 1,
|
|
46
|
+
openapi: { export: "npm run openapi:export", generated: "openapi/generated.json", published: "apps/docs-site/openapi/openapi.json" },
|
|
47
|
+
docsite: "apps/docs-site",
|
|
48
|
+
exclude: [],
|
|
49
|
+
requireHumanReview: [],
|
|
50
|
+
pathMappings: [],
|
|
51
|
+
devin: {
|
|
52
|
+
apiVersion: "v1",
|
|
53
|
+
unlisted: true,
|
|
54
|
+
maxAcuLimit: 2,
|
|
55
|
+
tags: ["docdrift"],
|
|
56
|
+
},
|
|
57
|
+
policy: {
|
|
58
|
+
prCaps: { maxPrsPerDay: 5, maxFilesTouched: 30 },
|
|
59
|
+
confidence: { autopatchThreshold: 0.8 },
|
|
60
|
+
allowlist: ["openapi/**", "apps/**"],
|
|
61
|
+
verification: { commands: ["npm run docs:gen", "npm run docs:build"] },
|
|
62
|
+
slaDays: 7,
|
|
63
|
+
slaLabel: "docdrift",
|
|
64
|
+
allowNewFiles: false,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
function buildConfigFromInference(inference, formResult) {
|
|
68
|
+
const base = deepMerge({ ...DEFAULT_CONFIG }, inference.suggestedConfig);
|
|
69
|
+
applyOverrides(base, formResult.configOverrides);
|
|
70
|
+
return base;
|
|
71
|
+
}
|
|
72
|
+
function writeConfig(config, outputPath) {
|
|
73
|
+
const dir = node_path_1.default.dirname(outputPath);
|
|
74
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
75
|
+
const yamlContent = [
|
|
76
|
+
"# yaml-language-server: $schema=./docdrift.schema.json",
|
|
77
|
+
js_yaml_1.default.dump(config, { lineWidth: 120, noRefs: true }),
|
|
78
|
+
].join("\n");
|
|
79
|
+
node_fs_1.default.writeFileSync(outputPath, yamlContent, "utf8");
|
|
80
|
+
}
|
|
81
|
+
function validateGeneratedConfig(configPath) {
|
|
82
|
+
try {
|
|
83
|
+
const content = node_fs_1.default.readFileSync(configPath, "utf8");
|
|
84
|
+
const parsed = js_yaml_1.default.load(content);
|
|
85
|
+
const result = schema_1.docDriftConfigSchema.safeParse(parsed);
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
const errors = result.error.errors.map((e) => `${e.path.join(".") || "root"}: ${e.message}`);
|
|
88
|
+
return { ok: false, errors };
|
|
89
|
+
}
|
|
90
|
+
return { ok: true, errors: [] };
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
return { ok: false, errors: [err instanceof Error ? err.message : String(err)] };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
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.runSetup = runSetup;
|
|
40
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
41
|
+
const repo_fingerprint_1 = require("./repo-fingerprint");
|
|
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");
|
|
47
|
+
async function runSetup(options = {}) {
|
|
48
|
+
const cwd = options.cwd ?? process.cwd();
|
|
49
|
+
const outputPath = node_path_1.default.resolve(cwd, options.outputPath ?? "docdrift.yaml");
|
|
50
|
+
const configExists = await Promise.resolve().then(() => __importStar(require("node:fs"))).then((fs) => fs.existsSync(outputPath));
|
|
51
|
+
if (configExists && !options.force) {
|
|
52
|
+
const { confirm } = await Promise.resolve().then(() => __importStar(require("@inquirer/prompts")));
|
|
53
|
+
const overwrite = await confirm({
|
|
54
|
+
message: "Config already exists. Overwrite?",
|
|
55
|
+
default: false,
|
|
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
|
+
}
|
|
91
|
+
console.log("\ndocdrift setup complete\n");
|
|
92
|
+
console.log(" docdrift.yaml written and validated");
|
|
93
|
+
for (const item of created) {
|
|
94
|
+
if (item === ".docdrift/")
|
|
95
|
+
console.log(" .docdrift/ created");
|
|
96
|
+
else if (item === "DocDrift.md")
|
|
97
|
+
console.log(" DocDrift.md created (edit for custom instructions)");
|
|
98
|
+
else if (item === ".gitignore")
|
|
99
|
+
console.log(" .gitignore updated");
|
|
100
|
+
else if (item.endsWith("docdrift.yml"))
|
|
101
|
+
console.log(" " + item + " added");
|
|
102
|
+
}
|
|
103
|
+
console.log("\nNext steps:");
|
|
104
|
+
console.log(" 1. Set DEVIN_API_KEY (local: .env or export; CI: repo secrets)");
|
|
105
|
+
console.log(" 2. Set GITHUB_TOKEN in repo secrets for PR comments and issues");
|
|
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)");
|
|
109
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runInteractiveForm = runInteractiveForm;
|
|
4
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
5
|
+
async function runInteractiveForm(inference, _cwd = process.cwd()) {
|
|
6
|
+
const configOverrides = {};
|
|
7
|
+
const skip = new Set(inference.skipQuestions ?? []);
|
|
8
|
+
for (const choice of inference.choices) {
|
|
9
|
+
if (skip.has(choice.key))
|
|
10
|
+
continue;
|
|
11
|
+
const options = choice.options;
|
|
12
|
+
if (options.length === 0)
|
|
13
|
+
continue;
|
|
14
|
+
const defaultOption = options[choice.defaultIndex] ?? options[0];
|
|
15
|
+
const choices = options.map((o, i) => ({
|
|
16
|
+
name: o.recommended ? `${o.label} (recommended)` : o.label,
|
|
17
|
+
value: o.value,
|
|
18
|
+
}));
|
|
19
|
+
const answer = await (0, prompts_1.select)({
|
|
20
|
+
message: choice.question,
|
|
21
|
+
choices,
|
|
22
|
+
default: defaultOption?.value,
|
|
23
|
+
});
|
|
24
|
+
configOverrides[choice.key] = answer;
|
|
25
|
+
}
|
|
26
|
+
const addCustomInstructions = await (0, prompts_1.confirm)({
|
|
27
|
+
message: "Add a custom instructions file for Devin? (PR titles, tone, project-specific guidance)",
|
|
28
|
+
default: true,
|
|
29
|
+
});
|
|
30
|
+
const addGitignore = await (0, prompts_1.confirm)({
|
|
31
|
+
message: "Add .docdrift artifact entries to .gitignore?",
|
|
32
|
+
default: true,
|
|
33
|
+
});
|
|
34
|
+
const addWorkflow = await (0, prompts_1.confirm)({
|
|
35
|
+
message: "Add GitHub Actions workflow for docdrift? (runs on push/PR to main)",
|
|
36
|
+
default: false,
|
|
37
|
+
});
|
|
38
|
+
const confirmed = await (0, prompts_1.confirm)({
|
|
39
|
+
message: "Write docdrift.yaml and complete setup? (will run validate)",
|
|
40
|
+
default: true,
|
|
41
|
+
});
|
|
42
|
+
if (!confirmed) {
|
|
43
|
+
throw new Error("Setup cancelled");
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
configOverrides,
|
|
47
|
+
onboarding: { addCustomInstructions, addGitignore, addWorkflow },
|
|
48
|
+
};
|
|
49
|
+
}
|