@devinnn/docdrift 0.1.3 → 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 CHANGED
@@ -1,5 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
3
36
  var __importDefault = (this && this.__importDefault) || function (mod) {
4
37
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
38
  };
@@ -17,7 +50,16 @@ function getArg(args, flag) {
17
50
  async function main() {
18
51
  const [, , command, ...args] = process.argv;
19
52
  if (!command) {
20
- throw new Error("Usage: docdrift <validate|detect|run|status|sla-check> [options]\n detect|run: [--base SHA] [--head SHA] (defaults: merge-base with main..HEAD)");
53
+ throw new Error("Usage: docdrift <validate|detect|run|status|sla-check|setup|generate-yaml> [options]\n detect|run: [--base SHA] [--head SHA] (defaults: merge-base with main..HEAD)\n setup|generate-yaml: [--output path] [--force]");
54
+ }
55
+ if (command === "setup" || command === "generate-yaml") {
56
+ require("dotenv").config();
57
+ const { runSetup } = await Promise.resolve().then(() => __importStar(require("./setup")));
58
+ await runSetup({
59
+ outputPath: getArg(args, "--output") ?? "docdrift.yaml",
60
+ force: args.includes("--force"),
61
+ });
62
+ return;
21
63
  }
22
64
  switch (command) {
23
65
  case "validate": {
@@ -91,12 +91,37 @@ async function devinListSessions(apiKey, params = {}) {
91
91
  }
92
92
  return [];
93
93
  }
94
+ const TERMINAL_STATUSES = [
95
+ "finished",
96
+ "blocked",
97
+ "error",
98
+ "cancelled",
99
+ "done",
100
+ "complete",
101
+ "completed",
102
+ "success",
103
+ "terminated",
104
+ ];
105
+ function hasPrUrl(session) {
106
+ if (typeof session.pull_request_url === "string" && session.pull_request_url)
107
+ return true;
108
+ if (typeof session.pr_url === "string" && session.pr_url)
109
+ return true;
110
+ const structured = (session.structured_output ?? session.data?.structured_output);
111
+ if (structured?.pr?.url)
112
+ return true;
113
+ return false;
114
+ }
94
115
  async function pollUntilTerminal(apiKey, sessionId, timeoutMs = 30 * 60_000) {
95
116
  const started = Date.now();
96
117
  while (Date.now() - started < timeoutMs) {
97
118
  const session = await devinGetSession(apiKey, sessionId);
98
119
  const status = String(session.status_enum ?? session.status ?? "UNKNOWN").toLowerCase();
99
- if (["finished", "blocked", "error", "cancelled", "done", "complete"].includes(status)) {
120
+ if (TERMINAL_STATUSES.includes(status)) {
121
+ return session;
122
+ }
123
+ // Session already produced a PR; stop polling so we don't timeout waiting for status to flip
124
+ if (hasPrUrl(session)) {
100
125
  return session;
101
126
  }
102
127
  await new Promise((resolve) => setTimeout(resolve, 5000));
package/dist/src/index.js CHANGED
@@ -36,7 +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.STATE_PATH = void 0;
39
+ exports.runSetup = exports.STATE_PATH = void 0;
40
40
  exports.runDetect = runDetect;
41
41
  exports.runDocDrift = runDocDrift;
42
42
  exports.runValidate = runValidate;
@@ -534,3 +534,5 @@ async function resolveBaseHead(baseArg, headArg) {
534
534
  return resolveDefaultBaseHead(headRef);
535
535
  }
536
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
+ }
@@ -0,0 +1,80 @@
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.ensureDocdriftDir = ensureDocdriftDir;
7
+ exports.createCustomInstructionsFile = createCustomInstructionsFile;
8
+ exports.ensureGitignore = ensureGitignore;
9
+ exports.addGitHubWorkflow = addGitHubWorkflow;
10
+ exports.runOnboarding = runOnboarding;
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const DOCDRIFT_DIR = ".docdrift";
14
+ const CUSTOM_INSTRUCTIONS_FILE = ".docdrift/DocDrift.md";
15
+ const GITIGNORE_BLOCK = `
16
+ # Docdrift run artifacts
17
+ .docdrift/evidence
18
+ .docdrift/*.log
19
+ .docdrift/state.json
20
+ .docdrift/run-output.json
21
+ `;
22
+ const WORKFLOW_CONTENT = "name: docdrift\n\non:\n push:\n branches: [\"main\"]\n pull_request:\n branches: [\"main\"]\n workflow_dispatch:\n\njobs:\n docdrift:\n runs-on: ubuntu-latest\n permissions:\n contents: write\n pull-requests: write\n issues: write\n steps:\n - uses: actions/checkout@v4\n with:\n fetch-depth: 0\n\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n\n - run: npm install\n\n - name: Determine SHAs\n id: shas\n run: |\n if [ \"${{ github.event_name }}\" = \"pull_request\" ]; then\n HEAD_SHA=\"${{ github.event.pull_request.head.sha }}\"\n BASE_SHA=\"${{ github.event.pull_request.base.sha }}\"\n else\n HEAD_SHA=\"${{ github.sha }}\"\n BASE_SHA=\"${{ github.event.before }}\"\n if [ -z \"$BASE_SHA\" ] || [ \"$BASE_SHA\" = \"0000000000000000000000000000000000000000\" ]; then\n BASE_SHA=\"$(git rev-parse HEAD^)\"\n fi\n fi\n echo \"head=${HEAD_SHA}\" >> $GITHUB_OUTPUT\n echo \"base=${BASE_SHA}\" >> $GITHUB_OUTPUT\n echo \"pr_number=${{ github.event.pull_request.number || '' }}\" >> $GITHUB_OUTPUT\n\n - name: Validate config\n run: npx docdrift validate\n\n - name: Run Doc Drift\n env:\n DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY }}\n GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n GITHUB_REPOSITORY: ${{ github.repository }}\n GITHUB_SHA: ${{ github.sha }}\n GITHUB_EVENT_NAME: ${{ github.event_name }}\n GITHUB_PR_NUMBER: ${{ steps.shas.outputs.pr_number }}\n run: |\n PR_ARGS=\"\"\n if [ -n \"$GITHUB_PR_NUMBER\" ]; then\n PR_ARGS=\"--trigger pull_request --pr-number $GITHUB_PR_NUMBER\"\n fi\n npx docdrift run --base ${{ steps.shas.outputs.base }} --head ${{ steps.shas.outputs.head }} $PR_ARGS\n\n - name: Upload artifacts\n if: always()\n uses: actions/upload-artifact@v4\n with:\n name: docdrift-artifacts\n path: |\n .docdrift/drift_report.json\n .docdrift/metrics.json\n .docdrift/run-output.json\n .docdrift/evidence/**\n .docdrift/state.json\n";
23
+ function ensureDocdriftDir(cwd) {
24
+ const dir = node_path_1.default.resolve(cwd, DOCDRIFT_DIR);
25
+ node_fs_1.default.mkdirSync(dir, { recursive: true });
26
+ }
27
+ const CUSTOM_INSTRUCTIONS_TEMPLATE = `# DocDrift custom instructions
28
+
29
+ - **PR titles:** Start every pull request title with \`[docdrift]\`.
30
+ - Add project-specific guidance for Devin here (e.g. terminology, tone, what to avoid).
31
+ `;
32
+ function createCustomInstructionsFile(cwd) {
33
+ const filePath = node_path_1.default.resolve(cwd, CUSTOM_INSTRUCTIONS_FILE);
34
+ ensureDocdriftDir(cwd);
35
+ node_fs_1.default.writeFileSync(filePath, CUSTOM_INSTRUCTIONS_TEMPLATE.trimStart(), "utf8");
36
+ }
37
+ const GITIGNORE_ENTRIES = [
38
+ ".docdrift/evidence",
39
+ ".docdrift/*.log",
40
+ ".docdrift/state.json",
41
+ ".docdrift/run-output.json",
42
+ ];
43
+ function hasGitignoreBlock(content) {
44
+ return GITIGNORE_ENTRIES.every((e) => content.includes(e));
45
+ }
46
+ function ensureGitignore(cwd) {
47
+ const gitignorePath = node_path_1.default.resolve(cwd, ".gitignore");
48
+ let content = "";
49
+ if (node_fs_1.default.existsSync(gitignorePath)) {
50
+ content = node_fs_1.default.readFileSync(gitignorePath, "utf8");
51
+ if (hasGitignoreBlock(content))
52
+ return;
53
+ }
54
+ const toAppend = content.endsWith("\n") ? GITIGNORE_BLOCK.trimStart() : GITIGNORE_BLOCK;
55
+ node_fs_1.default.writeFileSync(gitignorePath, content + toAppend, "utf8");
56
+ }
57
+ function addGitHubWorkflow(cwd) {
58
+ const workflowsDir = node_path_1.default.resolve(cwd, ".github", "workflows");
59
+ node_fs_1.default.mkdirSync(workflowsDir, { recursive: true });
60
+ const workflowPath = node_path_1.default.join(workflowsDir, "docdrift.yml");
61
+ node_fs_1.default.writeFileSync(workflowPath, WORKFLOW_CONTENT, "utf8");
62
+ }
63
+ function runOnboarding(cwd, choices) {
64
+ const created = [];
65
+ ensureDocdriftDir(cwd);
66
+ created.push(".docdrift/");
67
+ if (choices.addCustomInstructions) {
68
+ createCustomInstructionsFile(cwd);
69
+ created.push("DocDrift.md");
70
+ }
71
+ if (choices.addGitignore) {
72
+ ensureGitignore(cwd);
73
+ created.push(".gitignore");
74
+ }
75
+ if (choices.addWorkflow) {
76
+ addGitHubWorkflow(cwd);
77
+ created.push(".github/workflows/docdrift.yml");
78
+ }
79
+ return { created };
80
+ }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SYSTEM_PROMPT = void 0;
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
+
6
+ ## Docdrift config (simple mode)
7
+
8
+ Minimal valid config uses: version, openapi, docsite, pathMappings, devin, policy.
9
+
10
+ Example:
11
+ \`\`\`yaml
12
+ version: 1
13
+ openapi:
14
+ export: "npm run openapi:export"
15
+ generated: "openapi/generated.json"
16
+ published: "apps/docs-site/openapi/openapi.json"
17
+ docsite: "apps/docs-site"
18
+ pathMappings:
19
+ - match: "apps/api/**"
20
+ impacts: ["apps/docs-site/docs/**", "apps/docs-site/openapi/**"]
21
+ exclude: ["**/CHANGELOG*", "apps/docs-site/blog/**"]
22
+ requireHumanReview: []
23
+ devin:
24
+ apiVersion: v1
25
+ unlisted: true
26
+ maxAcuLimit: 2
27
+ tags: ["docdrift"]
28
+ policy:
29
+ prCaps: { maxPrsPerDay: 5, maxFilesTouched: 30 }
30
+ confidence: { autopatchThreshold: 0.8 }
31
+ allowlist: ["openapi/**", "apps/**"]
32
+ verification:
33
+ commands: ["npm run docs:gen", "npm run docs:build"]
34
+ slaDays: 7
35
+ slaLabel: docdrift
36
+ allowNewFiles: false
37
+ \`\`\`
38
+
39
+ ## Field rules
40
+
41
+ - openapi.export: Command to generate OpenAPI spec (e.g. "npm run openapi:export"). Prefer an existing script from root or workspace package.json.
42
+ - openapi.generated: Path where the export writes the spec (e.g. "openapi/generated.json").
43
+ - openapi.published: Path where the docsite consumes the spec (often under docsite, e.g. "apps/docs-site/openapi/openapi.json").
44
+ - docsite: Path to the docs site root (Docusaurus, Next.js docs, VitePress, MkDocs). Single string or array of strings.
45
+ - pathMappings: Array of { match, impacts }. match = glob for source/API code; impacts = globs for doc files that may need updates when match changes.
46
+ - policy.verification.commands: Commands to run after patching (e.g. "npm run docs:gen", "npm run docs:build"). Must exist in repo.
47
+ - exclude: Globs to never touch (e.g. blog, CHANGELOG).
48
+ - requireHumanReview: Globs that require human review when touched (e.g. guides).
49
+
50
+ ## Common patterns
51
+
52
+ - Docusaurus: docsite often has docusaurus.config.*; docs:gen may be "docusaurus -- gen-api-docs api"; openapi published path often under docsite/openapi/.
53
+ - Next/VitePress/MkDocs: docsite is the app root; look for docs/ or similar.
54
+
55
+ ## Output rules
56
+
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. "openapi.export"), question, options (array of { value, label, recommended? }), defaultIndex, help?, warning?, confidence ("high"|"medium"|"low").
59
+ 3. Add to skipQuestions the keys for which you are highly confident so the CLI will not ask the user.
60
+ 4. Prefer fewer, high-quality choices. If truly uncertain, set confidence to "low" and provide 2–3 options.
61
+ 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.`;
@@ -0,0 +1,155 @@
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.buildRepoFingerprint = buildRepoFingerprint;
7
+ exports.fingerprintHash = fingerprintHash;
8
+ const node_crypto_1 = __importDefault(require("node:crypto"));
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const IGNORE_DIRS = new Set(["node_modules", ".git", "dist", "build", "coverage", ".docdrift"]);
12
+ const DOC_HINTS = ["openapi", "swagger", "docusaurus", "mkdocs", "next", "vitepress"];
13
+ const MAX_TREE_DEPTH = 3;
14
+ function walkDir(dir, depth, tree) {
15
+ if (depth > MAX_TREE_DEPTH)
16
+ return;
17
+ let entries;
18
+ try {
19
+ entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
20
+ }
21
+ catch {
22
+ return;
23
+ }
24
+ const relDir = node_path_1.default.relative(process.cwd(), dir) || ".";
25
+ const names = [];
26
+ for (const e of entries) {
27
+ if (e.name.startsWith(".") && e.name !== ".env")
28
+ continue;
29
+ if (IGNORE_DIRS.has(e.name))
30
+ continue;
31
+ names.push(e.isDirectory() ? `${e.name}/` : e.name);
32
+ }
33
+ names.sort();
34
+ tree[relDir] = names;
35
+ for (const e of entries) {
36
+ if (!e.isDirectory() || IGNORE_DIRS.has(e.name))
37
+ continue;
38
+ walkDir(node_path_1.default.join(dir, e.name), depth + 1, tree);
39
+ }
40
+ }
41
+ function findMatchingFiles(cwd, test) {
42
+ const out = [];
43
+ function walk(dir, depth) {
44
+ if (depth > 5)
45
+ return;
46
+ let entries;
47
+ try {
48
+ entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
49
+ }
50
+ catch {
51
+ return;
52
+ }
53
+ for (const e of entries) {
54
+ if (e.name.startsWith(".") && e.name !== ".env")
55
+ continue;
56
+ if (IGNORE_DIRS.has(e.name))
57
+ continue;
58
+ const full = node_path_1.default.join(dir, e.name);
59
+ const rel = node_path_1.default.relative(cwd, full);
60
+ if (e.isFile() && test(rel, e.name))
61
+ out.push(rel);
62
+ else if (e.isDirectory())
63
+ walk(full, depth + 1);
64
+ }
65
+ }
66
+ walk(cwd, 0);
67
+ return out;
68
+ }
69
+ function findDirsNamed(cwd, name) {
70
+ const out = [];
71
+ function scan(dir, depth) {
72
+ if (depth > 2)
73
+ return;
74
+ let entries;
75
+ try {
76
+ entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
77
+ }
78
+ catch {
79
+ return;
80
+ }
81
+ for (const e of entries) {
82
+ if (e.name.startsWith(".") || IGNORE_DIRS.has(e.name))
83
+ continue;
84
+ const full = node_path_1.default.join(dir, e.name);
85
+ const rel = node_path_1.default.relative(cwd, full);
86
+ if (e.isDirectory()) {
87
+ if (e.name === name)
88
+ out.push(rel);
89
+ scan(full, depth + 1);
90
+ }
91
+ }
92
+ }
93
+ scan(cwd, 0);
94
+ return out;
95
+ }
96
+ function buildRepoFingerprint(cwd = process.cwd()) {
97
+ const fileTree = {};
98
+ walkDir(cwd, 0, fileTree);
99
+ let rootPackage = { scripts: {}, dependencies: [], workspaces: [] };
100
+ const pkgPath = node_path_1.default.join(cwd, "package.json");
101
+ if (node_fs_1.default.existsSync(pkgPath)) {
102
+ try {
103
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8"));
104
+ rootPackage.scripts = pkg.scripts || {};
105
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
106
+ rootPackage.dependencies = Object.keys(deps || {}).filter((k) => DOC_HINTS.some((h) => k.toLowerCase().includes(h)));
107
+ if (pkg.workspaces) {
108
+ rootPackage.workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : [pkg.workspaces];
109
+ }
110
+ }
111
+ catch {
112
+ // ignore
113
+ }
114
+ }
115
+ const workspacePackages = [];
116
+ if (rootPackage.workspaces?.length) {
117
+ for (const w of rootPackage.workspaces) {
118
+ const base = w.replace("/*", "").replace("*", "");
119
+ const dir = node_path_1.default.join(cwd, base);
120
+ if (!node_fs_1.default.existsSync(dir) || !node_fs_1.default.statSync(dir).isDirectory())
121
+ continue;
122
+ const subdirs = base.includes("*") ? node_fs_1.default.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => node_path_1.default.join(dir, e.name)) : [dir];
123
+ for (const sub of subdirs) {
124
+ const pj = node_path_1.default.join(sub, "package.json");
125
+ if (!node_fs_1.default.existsSync(pj))
126
+ continue;
127
+ try {
128
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(pj, "utf8"));
129
+ workspacePackages.push({
130
+ path: node_path_1.default.relative(cwd, sub),
131
+ scripts: pkg.scripts || {},
132
+ });
133
+ }
134
+ catch {
135
+ // ignore
136
+ }
137
+ }
138
+ }
139
+ }
140
+ const openapi = findMatchingFiles(cwd, (_, name) => /^openapi.*\.json$/i.test(name));
141
+ const swagger = findMatchingFiles(cwd, (_, name) => /^swagger.*\.json$/i.test(name));
142
+ const docusaurusConfig = findMatchingFiles(cwd, (_, name) => name.startsWith("docusaurus.config."));
143
+ const mkdocs = findMatchingFiles(cwd, (_, name) => name === "mkdocs.yml");
144
+ const docsDirs = findDirsNamed(cwd, "docs");
145
+ return {
146
+ fileTree,
147
+ rootPackage,
148
+ workspacePackages,
149
+ foundPaths: { openapi, swagger, docusaurusConfig, mkdocs, docsDirs },
150
+ };
151
+ }
152
+ function fingerprintHash(fingerprint) {
153
+ const canonical = JSON.stringify(fingerprint, Object.keys(fingerprint).sort());
154
+ return node_crypto_1.default.createHash("sha256").update(canonical).digest("hex");
155
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devinnn/docdrift",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "description": "Detect and remediate documentation drift with Devin sessions",
6
6
  "main": "dist/src/index.js",
@@ -42,7 +42,11 @@
42
42
  "prepublishOnly": "npm run build"
43
43
  },
44
44
  "dependencies": {
45
+ "@ai-sdk/gateway": "^1.0.0",
46
+ "@inquirer/prompts": "^7.2.0",
45
47
  "@octokit/rest": "^21.1.1",
48
+ "ai": "^4.0.0",
49
+ "dotenv": "^16.4.5",
46
50
  "fastify": "^5.2.1",
47
51
  "js-yaml": "^4.1.0",
48
52
  "zod": "^3.24.1"
@@ -57,4 +61,4 @@
57
61
  "vitest": "^3.0.5",
58
62
  "zod-to-json-schema": "^3.25.1"
59
63
  }
60
- }
64
+ }