driftfence 0.0.1
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/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/chunk-4RCBAET6.js +576 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +87 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +24 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DriftFence contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# DriftFence
|
|
2
|
+
|
|
3
|
+
[](https://github.com/CHAPAPOPA/driftfence/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Make sure your README doesn't lie.
|
|
6
|
+
|
|
7
|
+
DriftFence is a TypeScript Node.js CLI that catches outdated README and docs commands, package scripts, file references, and env var references before they reach users.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Once published to npm:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install -D driftfence
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
Check the current project:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npx driftfence check
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Check a specific project directory:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npx driftfence check ./path/to/project
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Example Output
|
|
32
|
+
|
|
33
|
+
The local clean demo exits with code 0:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npm run demo:clean
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The local drift demo intentionally contains documentation drift and exits with code 1:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npm run demo:drift
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Example output from the drift fixture:
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
DriftFence found documentation drift.
|
|
49
|
+
|
|
50
|
+
Package scripts:
|
|
51
|
+
- `npm run build` in README.md references missing package.json script `build`.
|
|
52
|
+
|
|
53
|
+
File paths:
|
|
54
|
+
- `docs/missing.md` referenced in README.md does not exist.
|
|
55
|
+
- `docs/advanced.md` referenced in docs/config.md does not exist.
|
|
56
|
+
|
|
57
|
+
Env vars:
|
|
58
|
+
- `DATABASE_URL` is used in src/index.ts but missing from .env.example.
|
|
59
|
+
|
|
60
|
+
4 issues found.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## MVP Checks
|
|
64
|
+
|
|
65
|
+
DriftFence checks `README.md` and `docs/**/*.md` for documentation drift.
|
|
66
|
+
|
|
67
|
+
Current checks:
|
|
68
|
+
|
|
69
|
+
- package script references
|
|
70
|
+
- file path references
|
|
71
|
+
- env var references in Markdown docs
|
|
72
|
+
- env var usage in source files
|
|
73
|
+
|
|
74
|
+
Package script references include commands like:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
npm run build
|
|
78
|
+
npm test
|
|
79
|
+
npm start
|
|
80
|
+
pnpm build
|
|
81
|
+
yarn build
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Env var checks currently support references like:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
API_URL
|
|
88
|
+
DATABASE_URL
|
|
89
|
+
VITE_API_URL
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
and source usages like:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
process.env.API_URL
|
|
96
|
+
import.meta.env.VITE_API_URL
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Exit Codes
|
|
100
|
+
|
|
101
|
+
DriftFence uses stable CLI exit codes:
|
|
102
|
+
|
|
103
|
+
- `0` — no documentation drift found
|
|
104
|
+
- `1` — documentation drift found
|
|
105
|
+
- `2` — invalid project directory or CLI usage error
|
|
106
|
+
|
|
107
|
+
## Roadmap
|
|
108
|
+
|
|
109
|
+
- MDX docs
|
|
110
|
+
- GitHub Action
|
|
111
|
+
- changed-files mode
|
|
112
|
+
- configurable ignore rules
|
|
113
|
+
- richer source analysis
|
|
114
|
+
|
|
115
|
+
## AI
|
|
116
|
+
|
|
117
|
+
AI features are not included in the MVP.
|
|
118
|
+
|
|
119
|
+
DriftFence is deterministic-first: it checks concrete references in docs and code instead of guessing.
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
// src/checkers/packageScripts.ts
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
var npmScriptPattern = /\bnpm\s+(?:run\s+([A-Za-z0-9:_-]+)|(start|test))\b/g;
|
|
5
|
+
var shorthandScriptPattern = /\b(pnpm|yarn)\s+(?:run\s+)?([A-Za-z0-9:_-]+)\b/g;
|
|
6
|
+
var packageManagerCommands = /* @__PURE__ */ new Set([
|
|
7
|
+
"add",
|
|
8
|
+
"audit",
|
|
9
|
+
"bin",
|
|
10
|
+
"cache",
|
|
11
|
+
"ci",
|
|
12
|
+
"config",
|
|
13
|
+
"create",
|
|
14
|
+
"dedupe",
|
|
15
|
+
"dlx",
|
|
16
|
+
"doctor",
|
|
17
|
+
"exec",
|
|
18
|
+
"explain",
|
|
19
|
+
"global",
|
|
20
|
+
"help",
|
|
21
|
+
"import",
|
|
22
|
+
"init",
|
|
23
|
+
"install",
|
|
24
|
+
"link",
|
|
25
|
+
"list",
|
|
26
|
+
"login",
|
|
27
|
+
"logout",
|
|
28
|
+
"node",
|
|
29
|
+
"outdated",
|
|
30
|
+
"pack",
|
|
31
|
+
"patch",
|
|
32
|
+
"plugin",
|
|
33
|
+
"publish",
|
|
34
|
+
"rebuild",
|
|
35
|
+
"remove",
|
|
36
|
+
"root",
|
|
37
|
+
"run",
|
|
38
|
+
"search",
|
|
39
|
+
"set",
|
|
40
|
+
"setup",
|
|
41
|
+
"store",
|
|
42
|
+
"uninstall",
|
|
43
|
+
"unlink",
|
|
44
|
+
"update",
|
|
45
|
+
"upgrade",
|
|
46
|
+
"view",
|
|
47
|
+
"why",
|
|
48
|
+
"workspace",
|
|
49
|
+
"workspaces"
|
|
50
|
+
]);
|
|
51
|
+
async function checkPackageScripts(projectRoot, references) {
|
|
52
|
+
const scriptReferences = findPackageScriptReferences(references);
|
|
53
|
+
if (scriptReferences.length === 0) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const packageJsonResult = await readPackageJson(projectRoot);
|
|
57
|
+
if ("issue" in packageJsonResult) {
|
|
58
|
+
return [packageJsonResult.issue];
|
|
59
|
+
}
|
|
60
|
+
const scripts = packageJsonResult.packageJson.scripts ?? {};
|
|
61
|
+
return scriptReferences.filter((reference) => !hasOwn(scripts, reference.script)).map((reference) => ({
|
|
62
|
+
type: "package-script",
|
|
63
|
+
path: reference.path,
|
|
64
|
+
command: reference.command,
|
|
65
|
+
script: reference.script
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
function findPackageScriptReferences(references) {
|
|
69
|
+
const scriptReferences = [];
|
|
70
|
+
for (const reference of references) {
|
|
71
|
+
scriptReferences.push(...findNpmScriptReferences(reference.value, reference.path));
|
|
72
|
+
scriptReferences.push(
|
|
73
|
+
...findShorthandScriptReferences(reference.value, reference.path)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return uniqueScriptReferences(scriptReferences);
|
|
77
|
+
}
|
|
78
|
+
function findNpmScriptReferences(text, path) {
|
|
79
|
+
const references = [];
|
|
80
|
+
for (const match of text.matchAll(npmScriptPattern)) {
|
|
81
|
+
const npmRunScript = match[1];
|
|
82
|
+
const npmTestScript = match[2];
|
|
83
|
+
const script = npmRunScript ?? npmTestScript;
|
|
84
|
+
if (!script) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
references.push({
|
|
88
|
+
path,
|
|
89
|
+
command: normalizeCommand(match[0]),
|
|
90
|
+
packageManager: "npm",
|
|
91
|
+
script
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return references;
|
|
95
|
+
}
|
|
96
|
+
function findShorthandScriptReferences(text, path) {
|
|
97
|
+
const references = [];
|
|
98
|
+
for (const match of text.matchAll(shorthandScriptPattern)) {
|
|
99
|
+
const packageManager = match[1];
|
|
100
|
+
const script = match[2];
|
|
101
|
+
if (packageManager !== "pnpm" && packageManager !== "yarn" || !script || packageManagerCommands.has(script)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
references.push({
|
|
105
|
+
path,
|
|
106
|
+
command: normalizeCommand(match[0]),
|
|
107
|
+
packageManager,
|
|
108
|
+
script
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return references;
|
|
112
|
+
}
|
|
113
|
+
async function readPackageJson(projectRoot) {
|
|
114
|
+
const path = "package.json";
|
|
115
|
+
const packageJsonPath = join(projectRoot, path);
|
|
116
|
+
let rawPackageJson;
|
|
117
|
+
try {
|
|
118
|
+
rawPackageJson = await readFile(packageJsonPath, "utf8");
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return {
|
|
121
|
+
issue: {
|
|
122
|
+
type: "package-json",
|
|
123
|
+
path,
|
|
124
|
+
message: getErrorCode(error) === "ENOENT" ? "package.json was not found." : `package.json could not be read: ${getErrorMessage(error)}`
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
return { packageJson: JSON.parse(rawPackageJson) };
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return {
|
|
132
|
+
issue: {
|
|
133
|
+
type: "package-json",
|
|
134
|
+
path,
|
|
135
|
+
message: `package.json is not valid JSON: ${getErrorMessage(error)}`
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function uniqueScriptReferences(references) {
|
|
141
|
+
const seen = /* @__PURE__ */ new Set();
|
|
142
|
+
const uniqueReferences = [];
|
|
143
|
+
for (const reference of references) {
|
|
144
|
+
const key = `${reference.path}:${reference.packageManager}:${reference.command}:${reference.script}`;
|
|
145
|
+
if (seen.has(key)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
seen.add(key);
|
|
149
|
+
uniqueReferences.push(reference);
|
|
150
|
+
}
|
|
151
|
+
return uniqueReferences;
|
|
152
|
+
}
|
|
153
|
+
function normalizeCommand(command) {
|
|
154
|
+
return command.replace(/\s+/g, " ").trim();
|
|
155
|
+
}
|
|
156
|
+
function hasOwn(object, key) {
|
|
157
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
158
|
+
}
|
|
159
|
+
function getErrorCode(error) {
|
|
160
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
161
|
+
}
|
|
162
|
+
function getErrorMessage(error) {
|
|
163
|
+
return error instanceof Error ? error.message : String(error);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/checkers/filePaths.ts
|
|
167
|
+
import { access } from "fs/promises";
|
|
168
|
+
import { resolve } from "path";
|
|
169
|
+
var pathTokenPattern = /(?:^|[\s"'`(])((?:\.{1,2}[\\/][^\s"'`()<>[\]{}]+|[A-Za-z0-9_.-]+[\\/][^\s"'`()<>[\]{}]+|\.[A-Za-z0-9_-][A-Za-z0-9_.-]*|[A-Za-z0-9_-]+\.[A-Za-z][A-Za-z0-9]{1,7})(?:[?#][A-Za-z0-9_.:/#?=&-]+)?)/g;
|
|
170
|
+
async function checkFilePaths(projectRoot, references) {
|
|
171
|
+
const pathReferences = findFilePathReferences(references);
|
|
172
|
+
const issues = [];
|
|
173
|
+
for (const reference of pathReferences) {
|
|
174
|
+
const absolutePath = resolve(projectRoot, reference.path);
|
|
175
|
+
try {
|
|
176
|
+
await access(absolutePath);
|
|
177
|
+
} catch {
|
|
178
|
+
issues.push({
|
|
179
|
+
type: "file-path",
|
|
180
|
+
path: reference.path,
|
|
181
|
+
markdownPath: reference.markdownPath
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return issues;
|
|
186
|
+
}
|
|
187
|
+
function findFilePathReferences(references) {
|
|
188
|
+
const pathReferences = [];
|
|
189
|
+
for (const reference of references) {
|
|
190
|
+
pathReferences.push(
|
|
191
|
+
...findFilePathReferencesInText(reference.value, reference.path)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return uniquePathReferences(pathReferences);
|
|
195
|
+
}
|
|
196
|
+
function findFilePathReferencesInText(text, markdownPath) {
|
|
197
|
+
const references = [];
|
|
198
|
+
for (const match of text.matchAll(pathTokenPattern)) {
|
|
199
|
+
const path = cleanPathCandidate(match[1]);
|
|
200
|
+
if (!isLikelyFilePath(path)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
references.push({ path, markdownPath });
|
|
204
|
+
}
|
|
205
|
+
return references;
|
|
206
|
+
}
|
|
207
|
+
function cleanPathCandidate(candidate) {
|
|
208
|
+
const path = (candidate ?? "").trim().replace(/^[`'"]+|[`'"]+$/g, "").replace(/[),.;:!?]+$/g, "").replace(/\\/g, "/");
|
|
209
|
+
const withoutUrlSuffix = path.split(/[?#]/)[0] ?? "";
|
|
210
|
+
return withoutUrlSuffix.replace(/:\d+(?::\d+)?$/g, "").replace(/^\.\//, "");
|
|
211
|
+
}
|
|
212
|
+
function isLikelyFilePath(path) {
|
|
213
|
+
if (path.length === 0 || path.startsWith("@") || path.startsWith("-") || path.includes("://") || path.includes("*") || path.includes("$")) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (!path.includes("/") && /^[A-Z][a-z]+(?:\.[a-z0-9]+)+$/.test(path)) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
return path.includes("/") || path.startsWith(".") || hasExtension(path);
|
|
220
|
+
}
|
|
221
|
+
function hasExtension(path) {
|
|
222
|
+
return /(^|[/\\])[^/\\]+\.[A-Za-z][A-Za-z0-9]{1,7}$/.test(path);
|
|
223
|
+
}
|
|
224
|
+
function uniquePathReferences(references) {
|
|
225
|
+
const seen = /* @__PURE__ */ new Set();
|
|
226
|
+
const uniqueReferences = [];
|
|
227
|
+
for (const reference of references) {
|
|
228
|
+
const key = `${reference.markdownPath}:${reference.path}`;
|
|
229
|
+
if (seen.has(key)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
seen.add(key);
|
|
233
|
+
uniqueReferences.push(reference);
|
|
234
|
+
}
|
|
235
|
+
return uniqueReferences;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/checkers/envVars.ts
|
|
239
|
+
import { readFile as readFile2, readdir } from "fs/promises";
|
|
240
|
+
import { join as join2, relative } from "path";
|
|
241
|
+
var envVarPattern = /\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g;
|
|
242
|
+
var processEnvPattern = /\bprocess\.env\.([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+)\b/g;
|
|
243
|
+
var importMetaEnvPattern = /\bimport\.meta\.env\.([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+)\b/g;
|
|
244
|
+
var sourceExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
245
|
+
async function checkEnvVars(projectRoot, markdownDocuments) {
|
|
246
|
+
const envExampleNames = await readEnvExampleNames(projectRoot);
|
|
247
|
+
const references = [
|
|
248
|
+
...markdownDocuments.flatMap(
|
|
249
|
+
(document) => findMarkdownEnvVarReferences(document)
|
|
250
|
+
),
|
|
251
|
+
...await findSourceEnvVarReferences(projectRoot)
|
|
252
|
+
];
|
|
253
|
+
return references.filter((reference) => !envExampleNames.has(reference.name)).map((reference) => ({
|
|
254
|
+
type: "env-var",
|
|
255
|
+
name: reference.name,
|
|
256
|
+
source: reference.source,
|
|
257
|
+
path: reference.path
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
function findEnvExampleNames(content) {
|
|
261
|
+
const names = /* @__PURE__ */ new Set();
|
|
262
|
+
for (const line of content.split(/\r?\n/)) {
|
|
263
|
+
const trimmedLine = line.trim();
|
|
264
|
+
if (trimmedLine.length === 0 || trimmedLine.startsWith("#")) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const match = /^([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+)\s*=/.exec(trimmedLine);
|
|
268
|
+
if (match?.[1]) {
|
|
269
|
+
names.add(match[1]);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return names;
|
|
273
|
+
}
|
|
274
|
+
function findMarkdownEnvVarReferences(document) {
|
|
275
|
+
return uniqueEnvVarReferences(
|
|
276
|
+
[...document.content.matchAll(envVarPattern)].map((match) => ({
|
|
277
|
+
name: match[0],
|
|
278
|
+
source: "markdown",
|
|
279
|
+
path: document.path
|
|
280
|
+
}))
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
async function readEnvExampleNames(projectRoot) {
|
|
284
|
+
try {
|
|
285
|
+
return findEnvExampleNames(
|
|
286
|
+
await readFile2(join2(projectRoot, ".env.example"), "utf8")
|
|
287
|
+
);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (getErrorCode2(error) === "ENOENT") {
|
|
290
|
+
return /* @__PURE__ */ new Set();
|
|
291
|
+
}
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function findSourceEnvVarReferences(projectRoot) {
|
|
296
|
+
const sourceRoot = join2(projectRoot, "src");
|
|
297
|
+
const sourceFiles = await findSourceFiles(sourceRoot);
|
|
298
|
+
const references = [];
|
|
299
|
+
for (const sourceFile of sourceFiles) {
|
|
300
|
+
const content = await readFile2(sourceFile, "utf8");
|
|
301
|
+
const path = normalizePath(relative(projectRoot, sourceFile));
|
|
302
|
+
references.push(
|
|
303
|
+
...findSourceEnvVarReferencesInText(content).map((reference) => ({
|
|
304
|
+
...reference,
|
|
305
|
+
path
|
|
306
|
+
}))
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
return uniqueEnvVarReferences(references);
|
|
310
|
+
}
|
|
311
|
+
function findSourceEnvVarReferencesInText(content) {
|
|
312
|
+
const references = [];
|
|
313
|
+
for (const match of content.matchAll(processEnvPattern)) {
|
|
314
|
+
if (match[1]) {
|
|
315
|
+
references.push({ name: match[1], source: "source", path: "" });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
for (const match of content.matchAll(importMetaEnvPattern)) {
|
|
319
|
+
if (match[1]) {
|
|
320
|
+
references.push({ name: match[1], source: "source", path: "" });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return references;
|
|
324
|
+
}
|
|
325
|
+
async function findSourceFiles(directory) {
|
|
326
|
+
let entries;
|
|
327
|
+
try {
|
|
328
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (getErrorCode2(error) === "ENOENT") {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
const files = [];
|
|
336
|
+
for (const entry of entries) {
|
|
337
|
+
const path = join2(directory, entry.name);
|
|
338
|
+
if (entry.isDirectory()) {
|
|
339
|
+
files.push(...await findSourceFiles(path));
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (entry.isFile() && sourceExtensions.has(getExtension(entry.name))) {
|
|
343
|
+
files.push(path);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return files;
|
|
347
|
+
}
|
|
348
|
+
function uniqueEnvVarReferences(references) {
|
|
349
|
+
const seen = /* @__PURE__ */ new Set();
|
|
350
|
+
const uniqueReferences = [];
|
|
351
|
+
for (const reference of references) {
|
|
352
|
+
const key = `${reference.source}:${reference.path}:${reference.name}`;
|
|
353
|
+
if (seen.has(key)) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
seen.add(key);
|
|
357
|
+
uniqueReferences.push(reference);
|
|
358
|
+
}
|
|
359
|
+
return uniqueReferences;
|
|
360
|
+
}
|
|
361
|
+
function getExtension(path) {
|
|
362
|
+
const index = path.lastIndexOf(".");
|
|
363
|
+
return index === -1 ? "" : path.slice(index);
|
|
364
|
+
}
|
|
365
|
+
function normalizePath(path) {
|
|
366
|
+
return path.replace(/\\/g, "/");
|
|
367
|
+
}
|
|
368
|
+
function getErrorCode2(error) {
|
|
369
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/markdown/extractMarkdownText.ts
|
|
373
|
+
import remarkParse from "remark-parse";
|
|
374
|
+
import { unified } from "unified";
|
|
375
|
+
function extractMarkdownText(markdown) {
|
|
376
|
+
const tree = unified().use(remarkParse).parse(markdown);
|
|
377
|
+
const references = [];
|
|
378
|
+
collectMarkdownText(tree, references);
|
|
379
|
+
return references;
|
|
380
|
+
}
|
|
381
|
+
function collectMarkdownText(node, references) {
|
|
382
|
+
if ((node.type === "code" || node.type === "inlineCode") && typeof node.value === "string") {
|
|
383
|
+
references.push({ kind: node.type, value: node.value });
|
|
384
|
+
}
|
|
385
|
+
if (!Array.isArray(node.children)) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
for (const child of node.children) {
|
|
389
|
+
collectMarkdownText(child, references);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/markdown/readMarkdownDocuments.ts
|
|
394
|
+
import { readFile as readFile3, readdir as readdir2 } from "fs/promises";
|
|
395
|
+
import { join as join3, relative as relative2 } from "path";
|
|
396
|
+
async function readMarkdownDocuments(projectRoot) {
|
|
397
|
+
const paths = await findMarkdownPaths(projectRoot);
|
|
398
|
+
const documents = [];
|
|
399
|
+
for (const path of paths) {
|
|
400
|
+
const content = await readFile3(join3(projectRoot, path), "utf8");
|
|
401
|
+
documents.push({
|
|
402
|
+
path,
|
|
403
|
+
content,
|
|
404
|
+
references: extractMarkdownText(content).map((reference) => ({
|
|
405
|
+
...reference,
|
|
406
|
+
path
|
|
407
|
+
}))
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return documents;
|
|
411
|
+
}
|
|
412
|
+
async function findMarkdownPaths(projectRoot) {
|
|
413
|
+
const paths = [];
|
|
414
|
+
if (await fileExists(join3(projectRoot, "README.md"))) {
|
|
415
|
+
paths.push("README.md");
|
|
416
|
+
}
|
|
417
|
+
paths.push(...await findDocsMarkdownPaths(projectRoot));
|
|
418
|
+
return paths;
|
|
419
|
+
}
|
|
420
|
+
async function findDocsMarkdownPaths(projectRoot) {
|
|
421
|
+
const docsRoot = join3(projectRoot, "docs");
|
|
422
|
+
const paths = await findMarkdownPathsInDirectory(docsRoot);
|
|
423
|
+
return paths.map((path) => normalizePath2(relative2(projectRoot, path))).sort((first, second) => first.localeCompare(second));
|
|
424
|
+
}
|
|
425
|
+
async function findMarkdownPathsInDirectory(directory) {
|
|
426
|
+
let entries;
|
|
427
|
+
try {
|
|
428
|
+
entries = await readdir2(directory, { withFileTypes: true });
|
|
429
|
+
} catch (error) {
|
|
430
|
+
if (getErrorCode3(error) === "ENOENT") {
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
const paths = [];
|
|
436
|
+
for (const entry of entries.sort(
|
|
437
|
+
(first, second) => first.name.localeCompare(second.name)
|
|
438
|
+
)) {
|
|
439
|
+
const path = join3(directory, entry.name);
|
|
440
|
+
if (entry.isDirectory()) {
|
|
441
|
+
paths.push(...await findMarkdownPathsInDirectory(path));
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
445
|
+
paths.push(path);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return paths;
|
|
449
|
+
}
|
|
450
|
+
async function fileExists(path) {
|
|
451
|
+
try {
|
|
452
|
+
await readFile3(path, "utf8");
|
|
453
|
+
return true;
|
|
454
|
+
} catch (error) {
|
|
455
|
+
if (getErrorCode3(error) === "ENOENT") {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function normalizePath2(path) {
|
|
462
|
+
return path.replace(/\\/g, "/");
|
|
463
|
+
}
|
|
464
|
+
function getErrorCode3(error) {
|
|
465
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/report/formatReport.ts
|
|
469
|
+
function formatReport(result) {
|
|
470
|
+
if (result.issues.length === 0) {
|
|
471
|
+
if (result.markdownDocuments.length === 0) {
|
|
472
|
+
return "DriftFence: no Markdown files found.";
|
|
473
|
+
}
|
|
474
|
+
return [
|
|
475
|
+
"DriftFence: no documentation drift found.",
|
|
476
|
+
`Checked ${result.markdownDocuments.map((document) => document.path).join(", ")}.`
|
|
477
|
+
].join("\n");
|
|
478
|
+
}
|
|
479
|
+
const projectIssues = result.issues.filter(isProjectIssue);
|
|
480
|
+
const packageScriptIssues = result.issues.filter(isPackageScriptIssue);
|
|
481
|
+
const filePathIssues = result.issues.filter(isFilePathIssue);
|
|
482
|
+
const envVarIssues = result.issues.filter(isEnvVarIssue);
|
|
483
|
+
const lines = ["DriftFence found documentation drift.", ""];
|
|
484
|
+
appendSection(
|
|
485
|
+
lines,
|
|
486
|
+
"Project",
|
|
487
|
+
projectIssues.map((issue) => `- ${issue.message}`)
|
|
488
|
+
);
|
|
489
|
+
appendSection(
|
|
490
|
+
lines,
|
|
491
|
+
"Package scripts",
|
|
492
|
+
packageScriptIssues.map(
|
|
493
|
+
(issue) => `- \`${issue.command}\` in ${issue.path} references missing package.json script \`${issue.script}\`.`
|
|
494
|
+
)
|
|
495
|
+
);
|
|
496
|
+
appendSection(
|
|
497
|
+
lines,
|
|
498
|
+
"File paths",
|
|
499
|
+
filePathIssues.map(
|
|
500
|
+
(issue) => `- \`${issue.path}\` referenced in ${issue.markdownPath} does not exist.`
|
|
501
|
+
)
|
|
502
|
+
);
|
|
503
|
+
appendSection(
|
|
504
|
+
lines,
|
|
505
|
+
"Env vars",
|
|
506
|
+
envVarIssues.map(
|
|
507
|
+
(issue) => issue.source === "markdown" ? `- \`${issue.name}\` is mentioned in ${issue.path} but missing from .env.example.` : `- \`${issue.name}\` is used in ${issue.path} but missing from .env.example.`
|
|
508
|
+
)
|
|
509
|
+
);
|
|
510
|
+
lines.push("");
|
|
511
|
+
lines.push(`${result.issues.length} ${pluralize("issue", result.issues.length)} found.`);
|
|
512
|
+
return lines.join("\n");
|
|
513
|
+
}
|
|
514
|
+
function appendSection(lines, title, entries) {
|
|
515
|
+
if (entries.length === 0) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
lines.push(`${title}:`);
|
|
519
|
+
lines.push(...entries);
|
|
520
|
+
lines.push("");
|
|
521
|
+
}
|
|
522
|
+
function isProjectIssue(issue) {
|
|
523
|
+
return issue.type === "package-json";
|
|
524
|
+
}
|
|
525
|
+
function isPackageScriptIssue(issue) {
|
|
526
|
+
return issue.type === "package-script";
|
|
527
|
+
}
|
|
528
|
+
function isFilePathIssue(issue) {
|
|
529
|
+
return issue.type === "file-path";
|
|
530
|
+
}
|
|
531
|
+
function isEnvVarIssue(issue) {
|
|
532
|
+
return issue.type === "env-var";
|
|
533
|
+
}
|
|
534
|
+
function pluralize(word, count) {
|
|
535
|
+
return count === 1 ? word : `${word}s`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/index.ts
|
|
539
|
+
async function checkProject(projectRoot = process.cwd()) {
|
|
540
|
+
const markdownDocuments = await readMarkdownDocuments(projectRoot);
|
|
541
|
+
const markdownReferences = markdownDocuments.flatMap(
|
|
542
|
+
(document) => document.references
|
|
543
|
+
);
|
|
544
|
+
if (markdownDocuments.length === 0) {
|
|
545
|
+
return {
|
|
546
|
+
projectRoot,
|
|
547
|
+
markdownDocuments,
|
|
548
|
+
markdownReferences,
|
|
549
|
+
issues: []
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
const [packageScriptIssues, filePathIssues, envVarIssues] = await Promise.all([
|
|
553
|
+
checkPackageScripts(projectRoot, markdownReferences),
|
|
554
|
+
checkFilePaths(projectRoot, markdownReferences),
|
|
555
|
+
checkEnvVars(projectRoot, markdownDocuments)
|
|
556
|
+
]);
|
|
557
|
+
return {
|
|
558
|
+
projectRoot,
|
|
559
|
+
markdownDocuments,
|
|
560
|
+
markdownReferences,
|
|
561
|
+
issues: [...packageScriptIssues, ...filePathIssues, ...envVarIssues]
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export {
|
|
566
|
+
checkPackageScripts,
|
|
567
|
+
findPackageScriptReferences,
|
|
568
|
+
checkFilePaths,
|
|
569
|
+
findFilePathReferences,
|
|
570
|
+
checkEnvVars,
|
|
571
|
+
findEnvExampleNames,
|
|
572
|
+
extractMarkdownText,
|
|
573
|
+
readMarkdownDocuments,
|
|
574
|
+
formatReport,
|
|
575
|
+
checkProject
|
|
576
|
+
};
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
checkProject,
|
|
4
|
+
formatReport
|
|
5
|
+
} from "./chunk-4RCBAET6.js";
|
|
6
|
+
|
|
7
|
+
// src/cli.ts
|
|
8
|
+
import { stat } from "fs/promises";
|
|
9
|
+
import { resolve } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import cac from "cac";
|
|
12
|
+
var defaultOutput = {
|
|
13
|
+
stdout: (message) => console.log(message),
|
|
14
|
+
stderr: (message) => console.error(message)
|
|
15
|
+
};
|
|
16
|
+
async function runCli(args = process.argv.slice(2), output = defaultOutput) {
|
|
17
|
+
const cli = cac("driftfence");
|
|
18
|
+
let exitCode = 0;
|
|
19
|
+
cli.command("check [projectDir]", "Check README.md for documentation drift").action(async (projectDir) => {
|
|
20
|
+
try {
|
|
21
|
+
const projectRoot = await resolveProjectRoot(projectDir);
|
|
22
|
+
const result = await checkProject(projectRoot);
|
|
23
|
+
output.stdout(formatReport(result));
|
|
24
|
+
exitCode = result.issues.length > 0 ? 1 : 0;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error instanceof ProjectDirectoryError) {
|
|
27
|
+
output.stderr(`DriftFence: ${error.message}`);
|
|
28
|
+
exitCode = 2;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
output.stderr(`DriftFence failed: ${getErrorMessage(error)}`);
|
|
32
|
+
exitCode = 1;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
cli.help();
|
|
36
|
+
cli.parse([process.execPath, fileURLToPath(import.meta.url), ...args], {
|
|
37
|
+
run: false
|
|
38
|
+
});
|
|
39
|
+
if (!cli.matchedCommand && !args.some(isHelpArgument)) {
|
|
40
|
+
cli.outputHelp();
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
await cli.runMatchedCommand();
|
|
44
|
+
return exitCode;
|
|
45
|
+
}
|
|
46
|
+
async function resolveProjectRoot(projectDir) {
|
|
47
|
+
const projectRoot = resolve(projectDir ?? process.cwd());
|
|
48
|
+
try {
|
|
49
|
+
const stats = await stat(projectRoot);
|
|
50
|
+
if (!stats.isDirectory()) {
|
|
51
|
+
throw new ProjectDirectoryError(
|
|
52
|
+
`Project path is not a directory: ${projectRoot}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return projectRoot;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error instanceof ProjectDirectoryError) {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
if (getErrorCode(error) === "ENOENT") {
|
|
61
|
+
throw new ProjectDirectoryError(
|
|
62
|
+
`Project directory does not exist: ${projectRoot}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
var ProjectDirectoryError = class extends Error {
|
|
69
|
+
};
|
|
70
|
+
function isDirectEntrypoint() {
|
|
71
|
+
return process.argv[1] ? resolve(process.argv[1]) === fileURLToPath(import.meta.url) : false;
|
|
72
|
+
}
|
|
73
|
+
function isHelpArgument(argument) {
|
|
74
|
+
return argument === "--help" || argument === "-h";
|
|
75
|
+
}
|
|
76
|
+
function getErrorCode(error) {
|
|
77
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
78
|
+
}
|
|
79
|
+
function getErrorMessage(error) {
|
|
80
|
+
return error instanceof Error ? error.message : String(error);
|
|
81
|
+
}
|
|
82
|
+
if (isDirectEntrypoint()) {
|
|
83
|
+
process.exitCode = await runCli();
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
runCli
|
|
87
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
type MarkdownTextKind = "code" | "inlineCode";
|
|
2
|
+
interface MarkdownTextReference {
|
|
3
|
+
kind: MarkdownTextKind;
|
|
4
|
+
value: string;
|
|
5
|
+
}
|
|
6
|
+
interface MarkdownTextReferenceWithPath extends MarkdownTextReference {
|
|
7
|
+
path: string;
|
|
8
|
+
}
|
|
9
|
+
declare function extractMarkdownText(markdown: string): MarkdownTextReference[];
|
|
10
|
+
|
|
11
|
+
interface PackageScriptReference {
|
|
12
|
+
path: string;
|
|
13
|
+
command: string;
|
|
14
|
+
packageManager: "npm" | "pnpm" | "yarn";
|
|
15
|
+
script: string;
|
|
16
|
+
}
|
|
17
|
+
interface PackageScriptIssue {
|
|
18
|
+
type: "package-script";
|
|
19
|
+
path: string;
|
|
20
|
+
command: string;
|
|
21
|
+
script: string;
|
|
22
|
+
}
|
|
23
|
+
interface PackageJsonIssue {
|
|
24
|
+
type: "package-json";
|
|
25
|
+
path: string;
|
|
26
|
+
message: string;
|
|
27
|
+
}
|
|
28
|
+
type PackageScriptCheckIssue = PackageScriptIssue | PackageJsonIssue;
|
|
29
|
+
declare function checkPackageScripts(projectRoot: string, references: MarkdownTextReferenceWithPath[]): Promise<PackageScriptCheckIssue[]>;
|
|
30
|
+
declare function findPackageScriptReferences(references: MarkdownTextReferenceWithPath[]): PackageScriptReference[];
|
|
31
|
+
|
|
32
|
+
interface FilePathReference {
|
|
33
|
+
path: string;
|
|
34
|
+
markdownPath: string;
|
|
35
|
+
}
|
|
36
|
+
interface FilePathIssue {
|
|
37
|
+
type: "file-path";
|
|
38
|
+
path: string;
|
|
39
|
+
markdownPath: string;
|
|
40
|
+
}
|
|
41
|
+
declare function checkFilePaths(projectRoot: string, references: MarkdownTextReferenceWithPath[]): Promise<FilePathIssue[]>;
|
|
42
|
+
declare function findFilePathReferences(references: MarkdownTextReferenceWithPath[]): FilePathReference[];
|
|
43
|
+
|
|
44
|
+
interface MarkdownDocument {
|
|
45
|
+
path: string;
|
|
46
|
+
content: string;
|
|
47
|
+
references: MarkdownTextReferenceWithPath[];
|
|
48
|
+
}
|
|
49
|
+
declare function readMarkdownDocuments(projectRoot: string): Promise<MarkdownDocument[]>;
|
|
50
|
+
|
|
51
|
+
interface EnvVarIssue {
|
|
52
|
+
type: "env-var";
|
|
53
|
+
name: string;
|
|
54
|
+
source: "markdown" | "source";
|
|
55
|
+
path: string;
|
|
56
|
+
}
|
|
57
|
+
declare function checkEnvVars(projectRoot: string, markdownDocuments: MarkdownDocument[]): Promise<EnvVarIssue[]>;
|
|
58
|
+
declare function findEnvExampleNames(content: string): Set<string>;
|
|
59
|
+
|
|
60
|
+
declare function formatReport(result: CheckResult): string;
|
|
61
|
+
|
|
62
|
+
type DriftIssue = PackageScriptCheckIssue | FilePathIssue | EnvVarIssue;
|
|
63
|
+
interface CheckResult {
|
|
64
|
+
projectRoot: string;
|
|
65
|
+
markdownDocuments: MarkdownDocument[];
|
|
66
|
+
markdownReferences: MarkdownTextReferenceWithPath[];
|
|
67
|
+
issues: DriftIssue[];
|
|
68
|
+
}
|
|
69
|
+
declare function checkProject(projectRoot?: string): Promise<CheckResult>;
|
|
70
|
+
|
|
71
|
+
export { type CheckResult, type DriftIssue, checkEnvVars, checkFilePaths, checkPackageScripts, checkProject, extractMarkdownText, findEnvExampleNames, findFilePathReferences, findPackageScriptReferences, formatReport, readMarkdownDocuments };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkEnvVars,
|
|
3
|
+
checkFilePaths,
|
|
4
|
+
checkPackageScripts,
|
|
5
|
+
checkProject,
|
|
6
|
+
extractMarkdownText,
|
|
7
|
+
findEnvExampleNames,
|
|
8
|
+
findFilePathReferences,
|
|
9
|
+
findPackageScriptReferences,
|
|
10
|
+
formatReport,
|
|
11
|
+
readMarkdownDocuments
|
|
12
|
+
} from "./chunk-4RCBAET6.js";
|
|
13
|
+
export {
|
|
14
|
+
checkEnvVars,
|
|
15
|
+
checkFilePaths,
|
|
16
|
+
checkPackageScripts,
|
|
17
|
+
checkProject,
|
|
18
|
+
extractMarkdownText,
|
|
19
|
+
findEnvExampleNames,
|
|
20
|
+
findFilePathReferences,
|
|
21
|
+
findPackageScriptReferences,
|
|
22
|
+
formatReport,
|
|
23
|
+
readMarkdownDocuments
|
|
24
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "driftfence",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"documentation",
|
|
8
|
+
"readme",
|
|
9
|
+
"drift",
|
|
10
|
+
"docs",
|
|
11
|
+
"developer-tools",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"description": "A CLI that catches outdated README commands, scripts, file references, and env vars.",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"driftfence": "./dist/cli.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"package.json"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"sideEffects": false,
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup src/cli.ts src/index.ts --format esm --dts --clean",
|
|
39
|
+
"demo:clean": "node ./dist/cli.js check ./tests/fixtures/basic-clean-project",
|
|
40
|
+
"demo:drift": "node ./dist/cli.js check ./tests/fixtures/basic-drift-project",
|
|
41
|
+
"dev": "tsx src/cli.ts",
|
|
42
|
+
"pack:dry": "npm pack --dry-run",
|
|
43
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
44
|
+
"test": "vitest run --pool=threads",
|
|
45
|
+
"typecheck": "tsc --noEmit"
|
|
46
|
+
},
|
|
47
|
+
"author": "Yakov Mulyavin",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/CHAPAPOPA/driftfence.git"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/CHAPAPOPA/driftfence/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/CHAPAPOPA/driftfence#readme",
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"cac": "^7.0.0",
|
|
58
|
+
"remark-parse": "^11.0.0",
|
|
59
|
+
"unified": "^11.0.5"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/node": "^26.1.0",
|
|
63
|
+
"tsup": "^8.5.1",
|
|
64
|
+
"tsx": "^4.23.0",
|
|
65
|
+
"typescript": "^6.0.3",
|
|
66
|
+
"vitest": "^4.1.10"
|
|
67
|
+
}
|
|
68
|
+
}
|