@devinnn/docdrift 0.1.11 → 0.1.13
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 +2 -1
- package/dist/src/devin/v1.js +8 -0
- package/dist/src/setup/ai-infer.js +96 -41
- package/dist/src/setup/devin-setup.js +122 -11
- package/dist/src/setup/generate-yaml.js +4 -14
- package/dist/src/setup/index.js +117 -12
- package/dist/src/setup/prompts.js +26 -80
- package/dist/src/setup/repo-fingerprint.js +153 -3
- package/dist/src/setup/setup-prompt.js +39 -4
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -57,7 +57,7 @@ async function main() {
|
|
|
57
57
|
" status Show run status [--since 24h]\n" +
|
|
58
58
|
" sla-check Check SLA for unmerged PRs\n" +
|
|
59
59
|
" setup Interactive setup (generates v2 docdrift.yaml)\n" +
|
|
60
|
-
" generate-yaml Generate config [--output path] [--force]");
|
|
60
|
+
" generate-yaml Generate config [--output path] [--force] [--open-pr]");
|
|
61
61
|
}
|
|
62
62
|
if (command === "setup" || command === "generate-yaml") {
|
|
63
63
|
require("dotenv").config();
|
|
@@ -65,6 +65,7 @@ async function main() {
|
|
|
65
65
|
await runSetup({
|
|
66
66
|
outputPath: getArg(args, "--output") ?? "docdrift.yaml",
|
|
67
67
|
force: args.includes("--force"),
|
|
68
|
+
openPr: args.includes("--open-pr"),
|
|
68
69
|
});
|
|
69
70
|
return;
|
|
70
71
|
}
|
package/dist/src/devin/v1.js
CHANGED
|
@@ -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
|
|
104
|
-
const
|
|
105
|
-
const openapiExport =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
:
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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: {
|
|
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
|
|
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,23 +64,125 @@ function getSchemaPath() {
|
|
|
60
64
|
}
|
|
61
65
|
return schemaPath;
|
|
62
66
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
}
|
|
104
|
+
/** Extract transcript text from session for fallback parsing */
|
|
105
|
+
function getSessionTranscript(session) {
|
|
106
|
+
const messages = session.messages ?? session.data?.messages;
|
|
107
|
+
if (!Array.isArray(messages))
|
|
108
|
+
return "";
|
|
109
|
+
return messages
|
|
110
|
+
.map((m) => (typeof m.content === "string" ? m.content : m.text ?? ""))
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.join("\n");
|
|
113
|
+
}
|
|
114
|
+
/** Parse setup output from <docdrift_setup_output>...</docdrift_setup_output> JSON block in text */
|
|
115
|
+
function parseFromStrictTag(text) {
|
|
116
|
+
const openTag = `<${setup_prompt_1.DOCDRIFT_SETUP_OUTPUT_TAG}>`;
|
|
117
|
+
const closeTag = `</${setup_prompt_1.DOCDRIFT_SETUP_OUTPUT_TAG}>`;
|
|
118
|
+
const openIdx = text.indexOf(openTag);
|
|
119
|
+
const closeIdx = text.indexOf(closeTag, openIdx);
|
|
120
|
+
if (openIdx === -1 || closeIdx === -1)
|
|
121
|
+
return null;
|
|
122
|
+
const inner = text.slice(openIdx + openTag.length, closeIdx).trim();
|
|
123
|
+
try {
|
|
124
|
+
const o = JSON.parse(inner);
|
|
125
|
+
const yaml = o.docdriftYaml;
|
|
126
|
+
const summary = o.summary;
|
|
127
|
+
if (typeof yaml !== "string" || typeof summary !== "string")
|
|
128
|
+
return null;
|
|
129
|
+
return {
|
|
130
|
+
docdriftYaml: yaml,
|
|
131
|
+
docDriftMd: typeof o.docDriftMd === "string" && o.docDriftMd ? o.docDriftMd : undefined,
|
|
132
|
+
workflowYml: typeof o.workflowYml === "string" && o.workflowYml ? o.workflowYml : undefined,
|
|
133
|
+
summary,
|
|
134
|
+
sessionUrl: "",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
66
138
|
return null;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Fallback: parse from markdown blocks like **docdriftYaml:** ```yaml ... ``` */
|
|
142
|
+
function parseFromMarkdownBlocks(text) {
|
|
143
|
+
const yamlMatch = text.match(/\*\*docdriftYaml:\*\*[\s\S]*?```(?:yaml)?\s*([\s\S]*?)```/i);
|
|
144
|
+
const docMdMatch = text.match(/\*\*docDriftMd:\*\*[\s\S]*?```(?:markdown)?\s*([\s\S]*?)```/i);
|
|
145
|
+
const workflowMatch = text.match(/\*\*workflowYml:\*\*[\s\S]*?```(?:yaml)?\s*([\s\S]*?)```/i);
|
|
146
|
+
const summaryBlock = text.match(/\*\*summary:\*\*([\s\S]*?)(?=\n\n\*\*|$)/i)?.[1]?.trim();
|
|
147
|
+
const yaml = yamlMatch?.[1]?.trim();
|
|
148
|
+
const summary = (summaryBlock || "Inferred from repo analysis").slice(0, 500);
|
|
149
|
+
if (!yaml)
|
|
71
150
|
return null;
|
|
72
151
|
return {
|
|
73
152
|
docdriftYaml: yaml,
|
|
74
|
-
docDriftMd:
|
|
75
|
-
workflowYml:
|
|
153
|
+
docDriftMd: docMdMatch?.[1]?.trim() || undefined,
|
|
154
|
+
workflowYml: workflowMatch?.[1]?.trim() || undefined,
|
|
76
155
|
summary,
|
|
77
156
|
sessionUrl: "",
|
|
78
157
|
};
|
|
79
158
|
}
|
|
159
|
+
function parseSetupOutput(session) {
|
|
160
|
+
const raw = session?.structured_output ?? session?.data?.structured_output;
|
|
161
|
+
if (raw && typeof raw === "object") {
|
|
162
|
+
const o = raw;
|
|
163
|
+
const yaml = o.docdriftYaml;
|
|
164
|
+
const summary = o.summary;
|
|
165
|
+
if (typeof yaml === "string" && typeof summary === "string") {
|
|
166
|
+
return {
|
|
167
|
+
docdriftYaml: yaml,
|
|
168
|
+
docDriftMd: typeof o.docDriftMd === "string" && o.docDriftMd ? o.docDriftMd : undefined,
|
|
169
|
+
workflowYml: typeof o.workflowYml === "string" && o.workflowYml ? o.workflowYml : undefined,
|
|
170
|
+
summary,
|
|
171
|
+
sessionUrl: "",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const transcript = getSessionTranscript(session);
|
|
176
|
+
if (transcript) {
|
|
177
|
+
const fromTag = parseFromStrictTag(transcript);
|
|
178
|
+
if (fromTag)
|
|
179
|
+
return fromTag;
|
|
180
|
+
const fromMarkdown = parseFromMarkdownBlocks(transcript);
|
|
181
|
+
if (fromMarkdown)
|
|
182
|
+
return fromMarkdown;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
80
186
|
async function runSetupDevin(options) {
|
|
81
187
|
const cwd = options.cwd ?? process.cwd();
|
|
82
188
|
const outputPath = node_path_1.default.resolve(cwd, options.outputPath ?? "docdrift.yaml");
|
|
@@ -98,7 +204,7 @@ async function runSetupDevin(options) {
|
|
|
98
204
|
process.stdout.write("Uploading schema…\n");
|
|
99
205
|
const schemaPath = getSchemaPath();
|
|
100
206
|
const attachmentUrl = await (0, v1_1.devinUploadAttachment)(apiKey, schemaPath);
|
|
101
|
-
const prompt = (0, setup_prompt_1.buildSetupPrompt)([attachmentUrl]);
|
|
207
|
+
const prompt = (0, setup_prompt_1.buildSetupPrompt)([attachmentUrl], { openPr: options.openPr });
|
|
102
208
|
process.stdout.write("Creating Devin session…\n");
|
|
103
209
|
const session = await (0, v1_1.devinCreateSession)(apiKey, {
|
|
104
210
|
prompt,
|
|
@@ -119,7 +225,12 @@ async function runSetupDevin(options) {
|
|
|
119
225
|
throw new Error("Devin session did not return valid setup output. Check the session for details: " + session.url);
|
|
120
226
|
}
|
|
121
227
|
result.sessionUrl = session.url;
|
|
122
|
-
|
|
228
|
+
const prUrl = finalSession.pull_request_url ??
|
|
229
|
+
finalSession.pr_url ??
|
|
230
|
+
finalSession.pull_request?.url;
|
|
231
|
+
if (prUrl)
|
|
232
|
+
result.prUrl = prUrl;
|
|
233
|
+
// Write files (always write for validation; when openPr, Devin also created PR)
|
|
123
234
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(outputPath), { recursive: true });
|
|
124
235
|
node_fs_1.default.writeFileSync(outputPath, result.docdriftYaml, "utf8");
|
|
125
236
|
if (result.docDriftMd) {
|
|
@@ -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/**"
|
|
98
|
-
verification: { commands: ["npm run
|
|
87
|
+
allowlist: ["openapi/**"],
|
|
88
|
+
verification: { commands: ["npm run build"] },
|
|
99
89
|
slaDays: 7,
|
|
100
90
|
slaLabel: "docdrift",
|
|
101
91
|
allowNewFiles: false,
|
package/dist/src/setup/index.js
CHANGED
|
@@ -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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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,30 @@ async function runSetup(options = {}) {
|
|
|
25
111
|
}
|
|
26
112
|
console.log(" .gitignore updated");
|
|
27
113
|
console.log("\nSummary: " + result.summary);
|
|
28
|
-
|
|
114
|
+
if (result.sessionUrl)
|
|
115
|
+
console.log("\nSession: " + result.sessionUrl);
|
|
116
|
+
if (result.prUrl)
|
|
117
|
+
console.log("PR: " + result.prUrl);
|
|
29
118
|
console.log("\nNext steps:");
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
119
|
+
const usedLocal = mode === "local" || (mode === "devin" && !hasDevinKey) || usedLocalFallback;
|
|
120
|
+
if (usedLocal) {
|
|
121
|
+
console.log(" 1. Run: npx @devinnn/docdrift validate — verify config");
|
|
122
|
+
console.log(" 2. Run: npx @devinnn/docdrift detect — check for drift");
|
|
123
|
+
if (usedLocalFallback) {
|
|
124
|
+
console.log(" 3. (Optional) Fix Devin and run setup again, or keep using local config");
|
|
125
|
+
}
|
|
126
|
+
else if (mode === "local") {
|
|
127
|
+
console.log(" 3. (Optional) Add repo to Devin and set DEVIN_API_KEY to use Devin for setup next time");
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
console.log(" 3. (Optional) Set DEVIN_API_KEY and run setup again to use Devin");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
console.log(" 1. Add DEVIN_API_KEY to repo secrets (Settings > Secrets > Actions)");
|
|
135
|
+
console.log(" 2. Ensure your repo is set up in Devin (Devin's Machine > Add repository)");
|
|
136
|
+
console.log(" 3. Run: npx @devinnn/docdrift validate — verify config");
|
|
137
|
+
console.log(" 4. Run: npx @devinnn/docdrift detect — check for drift");
|
|
138
|
+
console.log(" 5. Run: npx @devinnn/docdrift run — create Devin session (requires DEVIN_API_KEY)");
|
|
139
|
+
}
|
|
35
140
|
}
|
|
@@ -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
|
-
|
|
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: "
|
|
20
|
-
docsite: "
|
|
23
|
+
published: "{docsitePath}/openapi/openapi.json"
|
|
24
|
+
docsite: "{docsitePath}"
|
|
21
25
|
pathMappings:
|
|
22
|
-
- match: "
|
|
23
|
-
impacts: ["
|
|
24
|
-
exclude: ["**/CHANGELOG*", "
|
|
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/**", "
|
|
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:
|
|
47
|
-
- docsite: Path
|
|
48
|
-
- pathMappings:
|
|
49
|
-
- mode: "strict"
|
|
50
|
-
- policy.verification.commands: Commands
|
|
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
|
|
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:
|
|
67
|
-
-
|
|
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
|
|
72
|
-
2. For each field where confidence is medium or low,
|
|
73
|
-
3. Add to skipQuestions the keys for which you are highly confident
|
|
74
|
-
4.
|
|
75
|
-
5.
|
|
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, (
|
|
141
|
-
|
|
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: {
|
|
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) {
|
|
@@ -5,11 +5,46 @@
|
|
|
5
5
|
* Devin's Machine, so Devin has full context.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.DOCDRIFT_SETUP_OUTPUT_TAG = void 0;
|
|
8
9
|
exports.buildSetupPrompt = buildSetupPrompt;
|
|
10
|
+
/** XML tag used as strict delimiter for fallback parsing from chat transcript */
|
|
11
|
+
exports.DOCDRIFT_SETUP_OUTPUT_TAG = "docdrift_setup_output";
|
|
9
12
|
function attachmentBlock(urls) {
|
|
10
13
|
return urls.map((url, i) => `- ATTACHMENT ${i + 1}: ${url}`).join("\n");
|
|
11
14
|
}
|
|
12
|
-
function
|
|
15
|
+
function strictOutputBlock() {
|
|
16
|
+
return [
|
|
17
|
+
"",
|
|
18
|
+
"STRICT OUTPUT FORMAT (REQUIRED FOR PARSING):",
|
|
19
|
+
"You MUST include this exact block in your final message so we can reliably parse it.",
|
|
20
|
+
"Format: open with <" +
|
|
21
|
+
exports.DOCDRIFT_SETUP_OUTPUT_TAG +
|
|
22
|
+
">, then valid JSON, then close with </" +
|
|
23
|
+
exports.DOCDRIFT_SETUP_OUTPUT_TAG +
|
|
24
|
+
">.",
|
|
25
|
+
"Example (escape quotes in strings as \\\"):",
|
|
26
|
+
"",
|
|
27
|
+
`<${exports.DOCDRIFT_SETUP_OUTPUT_TAG}>`,
|
|
28
|
+
'{"docdriftYaml":"# yaml...","docDriftMd":"# DocDrift...","workflowYml":"name: docdrift...","summary":"OpenAPI at..."}',
|
|
29
|
+
`</${exports.DOCDRIFT_SETUP_OUTPUT_TAG}>`,
|
|
30
|
+
"",
|
|
31
|
+
"Rules: Valid JSON only. Newlines in YAML/yml strings become \\n. Escape \" as \\\".",
|
|
32
|
+
].join("\n");
|
|
33
|
+
}
|
|
34
|
+
function buildSetupPrompt(attachmentUrls, options) {
|
|
35
|
+
const openPr = options?.openPr ?? false;
|
|
36
|
+
const createFilesBlock = openPr
|
|
37
|
+
? [
|
|
38
|
+
"",
|
|
39
|
+
"CREATE A PULL REQUEST:",
|
|
40
|
+
"- Create branch docdrift/setup from main",
|
|
41
|
+
"- Create docdrift.yaml, .docdrift/DocDrift.md, .github/workflows/docdrift.yml in the repo",
|
|
42
|
+
"- Commit with message: [docdrift] Add docdrift configuration",
|
|
43
|
+
"- Push and open a PR to main with title: [docdrift] Add docdrift configuration",
|
|
44
|
+
"- In the PR description, explain what was inferred (openapi export, docsite path, verification commands)",
|
|
45
|
+
"- You MUST still emit the strict output block below so we can validate the config",
|
|
46
|
+
].join("\n")
|
|
47
|
+
: "Do NOT create files in the repo. Only produce the structured output and the strict output block.";
|
|
13
48
|
return [
|
|
14
49
|
"You are Devin. Task: set up docdrift for this repository.",
|
|
15
50
|
"",
|
|
@@ -43,12 +78,12 @@ function buildSetupPrompt(attachmentUrls) {
|
|
|
43
78
|
" - Note: docdrift-sla-check.yml (daily cron for PRs open 7+ days) is added automatically",
|
|
44
79
|
"",
|
|
45
80
|
"OUTPUT:",
|
|
46
|
-
"Emit your final output in the provided structured output schema.",
|
|
81
|
+
"Emit your final output in the provided structured output schema if possible.",
|
|
47
82
|
"- docdriftYaml: complete YAML string (no leading/trailing comments about the task)",
|
|
48
83
|
"- docDriftMd: content for .docdrift/DocDrift.md, or empty string to omit",
|
|
49
84
|
"- workflowYml: content for .github/workflows/docdrift.yml, or empty string to omit",
|
|
50
85
|
"- summary: what you inferred (openapi export, docsite path, verification commands)",
|
|
51
|
-
|
|
52
|
-
|
|
86
|
+
createFilesBlock,
|
|
87
|
+
strictOutputBlock(),
|
|
53
88
|
].join("\n");
|
|
54
89
|
}
|