@readme/cli 0.2.0 → 0.3.0
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/bin/git-format.js +80 -0
- package/package.json +23 -4
- package/src/commands/import.js +12 -3
- package/src/commands/oas-sync.js +185 -78
- package/src/commands/pretty.js +14 -4
- package/src/utils/containment.js +87 -0
- package/src/utils/reporter.js +1 -4
- package/src/validators/content.js +12 -5
- package/src/validators/frontmatter.js +4 -10
- package/src/validators/oas-reference.js +18 -4
- package/vendor/git-format/CLAUDE.md +352 -0
- package/vendor/git-format/frontmatter.schema.json +199 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Vendored copies of the two files this CLI reads from readmeio/git-format.
|
|
4
|
+
// Checked into the repo so installs do not fetch a git dependency (npm 12
|
|
5
|
+
// refuses those by default). `schemas:check` fails when upstream main drifts;
|
|
6
|
+
// `schemas:write` refreshes the copies.
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
const REPO = 'https://raw.githubusercontent.com/readmeio/git-format/main';
|
|
12
|
+
|
|
13
|
+
const files = [
|
|
14
|
+
{
|
|
15
|
+
filePath: 'vendor/git-format/frontmatter.schema.json',
|
|
16
|
+
url: `${REPO}/frontmatter.schema.json`,
|
|
17
|
+
json: true,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
filePath: 'vendor/git-format/CLAUDE.md',
|
|
21
|
+
url: `${REPO}/CLAUDE.md`,
|
|
22
|
+
json: false,
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function getHash(data) {
|
|
27
|
+
const text = typeof data === 'string' ? data : JSON.stringify(data);
|
|
28
|
+
return createHash('sha1').update(Buffer.from(text, 'utf8')).digest('hex');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// `writeFile` does not create missing parents. `schemas:write` is how those
|
|
32
|
+
// files get restored, so the directory has to be created first.
|
|
33
|
+
async function writeVendored(filePath, contents) {
|
|
34
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
35
|
+
await fs.writeFile(filePath, contents);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function refresh() {
|
|
39
|
+
const isUpdate = process.argv.includes('--update');
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await Promise.all(
|
|
43
|
+
files.map(async file => {
|
|
44
|
+
const res = await fetch(file.url);
|
|
45
|
+
if (!res.ok) throw new Error(`Received ${res.status} when attempting to fetch ${file.url}`);
|
|
46
|
+
|
|
47
|
+
if (file.json) {
|
|
48
|
+
const rawFile = await res.json();
|
|
49
|
+
if (isUpdate) {
|
|
50
|
+
await writeVendored(file.filePath, JSON.stringify(rawFile));
|
|
51
|
+
} else {
|
|
52
|
+
const currentData = JSON.parse(await fs.readFile(file.filePath, 'utf-8'));
|
|
53
|
+
if (getHash(currentData) !== getHash(rawFile)) {
|
|
54
|
+
throw new Error(`${file.filePath} is outdated! Run \`npm run schemas:write\` to update.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const rawFile = await res.text();
|
|
61
|
+
if (isUpdate) {
|
|
62
|
+
await writeVendored(file.filePath, rawFile);
|
|
63
|
+
} else {
|
|
64
|
+
const current = await fs.readFile(file.filePath, 'utf-8');
|
|
65
|
+
if (getHash(current) !== getHash(rawFile)) {
|
|
66
|
+
throw new Error(`${file.filePath} is outdated! Run \`npm run schemas:write\` to update.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
console.log(isUpdate ? 'git-format files updated!' : 'git-format files are up-to-date!');
|
|
73
|
+
process.exit(0);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error(e.message);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await refresh();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@readme/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The ReadMe CLI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -36,8 +36,10 @@
|
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build:gha": "ncc build src/gha.js -o dist-gha --external @readme/markdown",
|
|
38
38
|
"format": "npx oxlint --fix && npx oxfmt --write",
|
|
39
|
-
"lint": "npx oxlint && npx oxfmt --check .",
|
|
39
|
+
"lint": "npx oxlint && npx oxfmt --check . && npm run schemas:check",
|
|
40
40
|
"pretest": "npm run lint",
|
|
41
|
+
"schemas:check": "./bin/git-format.js",
|
|
42
|
+
"schemas:write": "./bin/git-format.js --update && npx oxfmt --write vendor/git-format/frontmatter.schema.json",
|
|
41
43
|
"start": "node bin/readme.js",
|
|
42
44
|
"test": "vitest run"
|
|
43
45
|
},
|
|
@@ -45,14 +47,13 @@
|
|
|
45
47
|
"@anthropic-ai/claude-agent-sdk": "^0.3.207",
|
|
46
48
|
"@readme/markdown": "^13.3.0",
|
|
47
49
|
"@readme/openapi-parser": "^5.5.0",
|
|
48
|
-
"@readmeio/git-format": "git+https://github.com/readmeio/git-format.git",
|
|
49
50
|
"ajv": "^8.18.0",
|
|
50
51
|
"chalk": "^5.4.1",
|
|
51
52
|
"cli-spinners": "^3.4.0",
|
|
52
53
|
"commander": "^13.1.0",
|
|
53
54
|
"js-yaml": "^5.5.0",
|
|
54
55
|
"log-update": "^8.0.0",
|
|
55
|
-
"next": "^15.
|
|
56
|
+
"next": "^15.5.29",
|
|
56
57
|
"oas-normalize": "^15.7.1",
|
|
57
58
|
"ora": "^9.3.0",
|
|
58
59
|
"react": "^18.3.1",
|
|
@@ -68,6 +69,24 @@
|
|
|
68
69
|
"oxlint": "^1.89.0",
|
|
69
70
|
"vitest": "^5.0.5"
|
|
70
71
|
},
|
|
72
|
+
"overrides": {
|
|
73
|
+
"@graphql-tools/executor-graphql-ws": {
|
|
74
|
+
"ws": "8.22.1"
|
|
75
|
+
},
|
|
76
|
+
"@graphql-tools/executor-legacy-ws": {
|
|
77
|
+
"ws": "8.22.1"
|
|
78
|
+
},
|
|
79
|
+
"cosmiconfig": {
|
|
80
|
+
"js-yaml": "4.3.2"
|
|
81
|
+
},
|
|
82
|
+
"express-rate-limit": {
|
|
83
|
+
"express": "4.22.3"
|
|
84
|
+
},
|
|
85
|
+
"graphql-config": {
|
|
86
|
+
"minimatch": "4.2.6"
|
|
87
|
+
},
|
|
88
|
+
"uuid": "^11.1.1"
|
|
89
|
+
},
|
|
71
90
|
"engines": {
|
|
72
91
|
"node": ">=22"
|
|
73
92
|
}
|
package/src/commands/import.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
organizeFromScratchPrompt,
|
|
19
19
|
organizeFromScratchOutputSchema,
|
|
20
20
|
} from '../prompts/index.js';
|
|
21
|
+
import { makeContainment, PathEscapeError } from '../utils/containment.js';
|
|
21
22
|
import { analyzeLlmsTxt } from '../utils/llms.js';
|
|
22
23
|
import { parseFrontmatter, toFrontmatter } from '../utils/markdown.js';
|
|
23
24
|
import * as styles from '../utils/styles.js';
|
|
@@ -1322,6 +1323,11 @@ export async function runAgent({ userPrompt, systemPrompt, cwd, model }) {
|
|
|
1322
1323
|
|
|
1323
1324
|
function makeStagingGuard(stagingDir) {
|
|
1324
1325
|
const absStaging = path.resolve(stagingDir);
|
|
1326
|
+
|
|
1327
|
+
// `path.relative` is lexical, so a symlink planted in the staging dir
|
|
1328
|
+
// (or a path whose existing ancestor is one) would look inside it while
|
|
1329
|
+
// the Write tool follows the link. Same containment check as oas:sync.
|
|
1330
|
+
const assertContained = makeContainment(stagingDir, { label: 'staging directory' });
|
|
1325
1331
|
const WRITE_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit', 'MultiEdit']);
|
|
1326
1332
|
|
|
1327
1333
|
return async (toolName, input) => {
|
|
@@ -1330,10 +1336,13 @@ function makeStagingGuard(stagingDir) {
|
|
|
1330
1336
|
if (typeof fp !== 'string' || !fp) {
|
|
1331
1337
|
return { behavior: 'deny', message: `${toolName}: missing file_path` };
|
|
1332
1338
|
}
|
|
1339
|
+
|
|
1333
1340
|
const abs = path.isAbsolute(fp) ? path.resolve(fp) : path.resolve(absStaging, fp);
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1341
|
+
|
|
1342
|
+
try {
|
|
1343
|
+
assertContained(abs);
|
|
1344
|
+
} catch (err) {
|
|
1345
|
+
if (!(err instanceof PathEscapeError)) throw err;
|
|
1337
1346
|
styles.warning(`Blocked ${toolName} outside staging: ${fp}`);
|
|
1338
1347
|
return {
|
|
1339
1348
|
behavior: 'deny',
|