docspack 0.0.1 → 0.1.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 +179 -0
- package/bin/docspack.js +25 -0
- package/dist/build.d.ts +31 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +435 -0
- package/dist/build.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +763 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +118 -0
- package/dist/config.js.map +1 -0
- package/dist/db.d.ts +60 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +204 -0
- package/dist/db.js.map +1 -0
- package/dist/discovery.d.ts +31 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +126 -0
- package/dist/discovery.js.map +1 -0
- package/dist/doctor.d.ts +25 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +276 -0
- package/dist/doctor.js.map +1 -0
- package/dist/document.d.ts +13 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +47 -0
- package/dist/document.js.map +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -0
- package/dist/exports.d.ts +20 -0
- package/dist/exports.d.ts.map +1 -0
- package/dist/exports.js +100 -0
- package/dist/exports.js.map +1 -0
- package/dist/feedback.d.ts +68 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +0 -0
- package/dist/feedback.js.map +1 -0
- package/dist/html.d.ts +4 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +23 -0
- package/dist/html.js.map +1 -0
- package/dist/http.d.ts +30 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +144 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/init/detect.d.ts +16 -0
- package/dist/init/detect.d.ts.map +1 -0
- package/dist/init/detect.js +120 -0
- package/dist/init/detect.js.map +1 -0
- package/dist/init/plan.d.ts +43 -0
- package/dist/init/plan.d.ts.map +1 -0
- package/dist/init/plan.js +145 -0
- package/dist/init/plan.js.map +1 -0
- package/dist/init/run.d.ts +28 -0
- package/dist/init/run.d.ts.map +1 -0
- package/dist/init/run.js +96 -0
- package/dist/init/run.js.map +1 -0
- package/dist/init/templates.d.ts +24 -0
- package/dist/init/templates.d.ts.map +1 -0
- package/dist/init/templates.js +181 -0
- package/dist/init/templates.js.map +1 -0
- package/dist/init/write.d.ts +20 -0
- package/dist/init/write.d.ts.map +1 -0
- package/dist/init/write.js +56 -0
- package/dist/init/write.js.map +1 -0
- package/dist/kinds.d.ts +14 -0
- package/dist/kinds.d.ts.map +1 -0
- package/dist/kinds.js +15 -0
- package/dist/kinds.js.map +1 -0
- package/dist/llms-txt.d.ts +25 -0
- package/dist/llms-txt.d.ts.map +1 -0
- package/dist/llms-txt.js +94 -0
- package/dist/llms-txt.js.map +1 -0
- package/dist/mcp.d.ts +15 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +158 -0
- package/dist/mcp.js.map +1 -0
- package/dist/preview.d.ts +18 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/preview.js +72 -0
- package/dist/preview.js.map +1 -0
- package/dist/prompt.d.ts +27 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +79 -0
- package/dist/prompt.js.map +1 -0
- package/dist/search.d.ts +41 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +60 -0
- package/dist/search.js.map +1 -0
- package/dist/snippet.d.ts +20 -0
- package/dist/snippet.d.ts.map +1 -0
- package/dist/snippet.js +29 -0
- package/dist/snippet.js.map +1 -0
- package/dist/spec.d.ts +38 -0
- package/dist/spec.d.ts.map +1 -0
- package/dist/spec.js +105 -0
- package/dist/spec.js.map +1 -0
- package/dist/style.d.ts +33 -0
- package/dist/style.d.ts.map +1 -0
- package/dist/style.js +94 -0
- package/dist/style.js.map +1 -0
- package/dist/submit.d.ts +61 -0
- package/dist/submit.d.ts.map +1 -0
- package/dist/submit.js +111 -0
- package/dist/submit.js.map +1 -0
- package/dist/sync.d.ts +29 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +73 -0
- package/dist/sync.js.map +1 -0
- package/dist/verify.d.ts +44 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +291 -0
- package/dist/verify.js.map +1 -0
- package/package.json +61 -5
- package/src/build.ts +572 -0
- package/src/cli.ts +883 -0
- package/src/config.ts +158 -0
- package/src/db.ts +261 -0
- package/src/discovery.ts +161 -0
- package/src/doctor.ts +344 -0
- package/src/document.ts +59 -0
- package/src/errors.ts +10 -0
- package/src/exports.ts +120 -0
- package/src/feedback.ts +0 -0
- package/src/html.ts +24 -0
- package/src/http.ts +190 -0
- package/src/index.ts +132 -0
- package/src/init/detect.ts +142 -0
- package/src/init/plan.ts +215 -0
- package/src/init/run.ts +142 -0
- package/src/init/templates.ts +200 -0
- package/src/init/write.ts +83 -0
- package/src/kinds.ts +17 -0
- package/src/llms-txt.ts +116 -0
- package/src/mcp.ts +196 -0
- package/src/preview.ts +98 -0
- package/src/prompt.ts +103 -0
- package/src/search.ts +96 -0
- package/src/snippet.ts +30 -0
- package/src/spec.ts +138 -0
- package/src/style.ts +111 -0
- package/src/submit.ts +189 -0
- package/src/sync.ts +112 -0
- package/src/verify.ts +355 -0
- package/bin/cli.js +0 -2
package/src/init/run.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { type BuildResult, buildPackage } from "../build.js";
|
|
3
|
+
import { type DoctorReport, runDoctor } from "../doctor.js";
|
|
4
|
+
import { Prompter } from "../prompt.js";
|
|
5
|
+
import { type Detected, detectProject } from "./detect.js";
|
|
6
|
+
import { type InitOptions, type InitPlan, planInit, proposePackageName } from "./plan.js";
|
|
7
|
+
import { applyPlan, type WrittenFile } from "./write.js";
|
|
8
|
+
|
|
9
|
+
export interface RunInitOptions extends InitOptions {
|
|
10
|
+
readonly cwd: string;
|
|
11
|
+
/** Skip every prompt and use flags plus detected defaults. */
|
|
12
|
+
readonly yes?: boolean;
|
|
13
|
+
readonly dryRun?: boolean;
|
|
14
|
+
readonly force?: boolean;
|
|
15
|
+
/** Run the first build after scaffolding. Defaults to true. */
|
|
16
|
+
readonly build?: boolean;
|
|
17
|
+
readonly prompter?: Prompter;
|
|
18
|
+
readonly onProgress?: (message: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface InitResult {
|
|
22
|
+
readonly detected: Detected;
|
|
23
|
+
readonly plan: InitPlan;
|
|
24
|
+
readonly written: readonly WrittenFile[];
|
|
25
|
+
readonly build?: BuildResult;
|
|
26
|
+
readonly doctor?: DoctorReport;
|
|
27
|
+
readonly cancelled: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** detect → propose → confirm → write → build → doctor. */
|
|
31
|
+
export async function runInit(options: RunInitOptions): Promise<InitResult> {
|
|
32
|
+
const detected = await detectProject(options.cwd);
|
|
33
|
+
const prompter = options.prompter ?? new Prompter();
|
|
34
|
+
const interactive = options.yes !== true && prompter.interactive;
|
|
35
|
+
|
|
36
|
+
const answers = interactive ? await ask(prompter, options, detected) : options;
|
|
37
|
+
const plan = planInit(answers, detected);
|
|
38
|
+
|
|
39
|
+
if (interactive) {
|
|
40
|
+
prompter.note("");
|
|
41
|
+
for (const note of plan.notes) prompter.note(` · ${note}`);
|
|
42
|
+
prompter.note("");
|
|
43
|
+
if (!(await prompter.confirm(`Write ${plan.files.length} files to ${plan.dir}/?`))) {
|
|
44
|
+
prompter.close();
|
|
45
|
+
return { detected, plan, written: [], cancelled: true };
|
|
46
|
+
}
|
|
47
|
+
prompter.close();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const written = await applyPlan(options.cwd, plan, {
|
|
51
|
+
...(options.force === undefined ? {} : { force: options.force }),
|
|
52
|
+
...(options.dryRun === undefined ? {} : { dryRun: options.dryRun }),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (options.dryRun === true || options.build === false) {
|
|
56
|
+
return { detected, plan, written, cancelled: false };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const dir = join(options.cwd, plan.dir);
|
|
60
|
+
const built = await buildPackage({
|
|
61
|
+
out: dir,
|
|
62
|
+
...(options.onProgress === undefined ? {} : { onProgress: options.onProgress }),
|
|
63
|
+
});
|
|
64
|
+
const doctor = await runDoctor({ dir });
|
|
65
|
+
|
|
66
|
+
return { detected, plan, written, build: built, doctor, cancelled: false };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function ask(
|
|
70
|
+
prompter: Prompter,
|
|
71
|
+
options: RunInitOptions,
|
|
72
|
+
detected: Detected,
|
|
73
|
+
): Promise<InitOptions> {
|
|
74
|
+
const community = options.community === true;
|
|
75
|
+
const proposed =
|
|
76
|
+
options.name ??
|
|
77
|
+
(detected.libraryName === undefined ? undefined : proposePackageName(detected, community));
|
|
78
|
+
|
|
79
|
+
if (detected.libraryName !== undefined) {
|
|
80
|
+
const where = [
|
|
81
|
+
`${detected.libraryName}${detected.libraryVersion === undefined ? "" : `@${detected.libraryVersion}`}`,
|
|
82
|
+
detected.docsDir === undefined
|
|
83
|
+
? undefined
|
|
84
|
+
: `${detected.docsDir}/ (${detected.docsFiles} files)`,
|
|
85
|
+
detected.openapi,
|
|
86
|
+
].filter((part): part is string => part !== undefined);
|
|
87
|
+
prompter.note(`Detected ${where.join(" · ")}\n`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const name = await prompter.text("Package name", proposed ?? "@acme/docspack");
|
|
91
|
+
const version = await prompter.text(
|
|
92
|
+
"Version",
|
|
93
|
+
options.version ?? detected.libraryVersion ?? "0.1.0",
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const input = await selectInput(prompter, options, detected);
|
|
97
|
+
const out = await prompter.text("Directory", options.out ?? "docspack");
|
|
98
|
+
const workflow = await prompter.confirm("Add the release workflow?", options.workflow !== false);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
...options,
|
|
102
|
+
name,
|
|
103
|
+
version,
|
|
104
|
+
out,
|
|
105
|
+
workflow,
|
|
106
|
+
...input,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function selectInput(
|
|
111
|
+
prompter: Prompter,
|
|
112
|
+
options: RunInitOptions,
|
|
113
|
+
detected: Detected,
|
|
114
|
+
): Promise<Pick<InitOptions, "from" | "openapi" | "mirror">> {
|
|
115
|
+
if (options.from !== undefined) return { from: options.from };
|
|
116
|
+
if (options.openapi !== undefined) return { openapi: options.openapi };
|
|
117
|
+
if (options.mirror !== undefined) return { mirror: options.mirror };
|
|
118
|
+
|
|
119
|
+
const choices: {
|
|
120
|
+
value: Pick<InitOptions, "from" | "openapi" | "mirror">;
|
|
121
|
+
label: string;
|
|
122
|
+
hint?: string;
|
|
123
|
+
}[] = [];
|
|
124
|
+
if (detected.docsDir !== undefined) {
|
|
125
|
+
choices.push({
|
|
126
|
+
value: { from: detected.docsDir },
|
|
127
|
+
label: `${detected.docsDir}/`,
|
|
128
|
+
hint: `${detected.docsFiles} Markdown files`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (detected.openapi !== undefined) {
|
|
132
|
+
choices.push({
|
|
133
|
+
value: { openapi: detected.openapi },
|
|
134
|
+
label: detected.openapi,
|
|
135
|
+
hint: "one chunk per operation",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
choices.push({ value: {}, label: "start from templates", hint: "seeds docs/ with three files" });
|
|
139
|
+
|
|
140
|
+
if (choices.length === 1) return {};
|
|
141
|
+
return prompter.select("Documentation source", choices);
|
|
142
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { CONFIG_KEY } from "../config.js";
|
|
2
|
+
import { AGENTS_SNIPPET, FEEDBACK_SNIPPET } from "../snippet.js";
|
|
3
|
+
import { LLMS_DIR } from "../spec.js";
|
|
4
|
+
|
|
5
|
+
export interface PackageJsonInput {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly version: string;
|
|
8
|
+
readonly libraryName?: string;
|
|
9
|
+
readonly repository?: string;
|
|
10
|
+
readonly license?: string;
|
|
11
|
+
readonly build: Record<string, string | number>;
|
|
12
|
+
readonly docspackVersion: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Marker the doctor looks for, so a half-filled template cannot be published as documentation. */
|
|
16
|
+
export const PLACEHOLDER = "TODO:";
|
|
17
|
+
|
|
18
|
+
export function packageJson(input: PackageJsonInput): string {
|
|
19
|
+
const subject = input.libraryName ?? input.name;
|
|
20
|
+
return `${JSON.stringify(
|
|
21
|
+
{
|
|
22
|
+
name: input.name,
|
|
23
|
+
version: input.version,
|
|
24
|
+
description: `Documentation for ${subject}, for use with docspack.`,
|
|
25
|
+
keywords: ["docspack", "documentation", "ai", "agents", "mcp"],
|
|
26
|
+
...(input.repository === undefined
|
|
27
|
+
? {}
|
|
28
|
+
: { repository: { type: "git", url: `git+${input.repository}.git` } }),
|
|
29
|
+
...(input.license === undefined ? {} : { license: input.license }),
|
|
30
|
+
// Without .llms the published package installs fine and indexes nothing.
|
|
31
|
+
files: [LLMS_DIR, "llms.txt"],
|
|
32
|
+
publishConfig: { access: "public" },
|
|
33
|
+
// `documents` names the library, which is what `docspack verify` checks the docs against.
|
|
34
|
+
[CONFIG_KEY]: {
|
|
35
|
+
...(input.libraryName === undefined ? {} : { documents: input.libraryName }),
|
|
36
|
+
...input.build,
|
|
37
|
+
},
|
|
38
|
+
scripts: {
|
|
39
|
+
build: "docspack build",
|
|
40
|
+
// Rebuilds and validates on every publish, so a stale payload cannot reach the registry.
|
|
41
|
+
prepublishOnly: "docspack build && docspack doctor --strict",
|
|
42
|
+
},
|
|
43
|
+
devDependencies: { docspack: `^${input.docspackVersion}` },
|
|
44
|
+
},
|
|
45
|
+
null,
|
|
46
|
+
2,
|
|
47
|
+
)}\n`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readme(name: string, libraryName: string | undefined): string {
|
|
51
|
+
const subject = libraryName ?? "this library";
|
|
52
|
+
return `# ${name}
|
|
53
|
+
|
|
54
|
+
Documentation for ${subject}, packaged for [docspack](https://github.com/docspack/docspack)
|
|
55
|
+
so AI coding agents can read it locally, at the version you installed.
|
|
56
|
+
|
|
57
|
+
\`\`\`bash
|
|
58
|
+
npm i -D ${name}
|
|
59
|
+
npx docspack sync
|
|
60
|
+
npx docspack ask "how do I authenticate"
|
|
61
|
+
\`\`\`
|
|
62
|
+
|
|
63
|
+
Give an agent access with one line in AGENTS.md or CLAUDE.md:
|
|
64
|
+
|
|
65
|
+
\`\`\`md
|
|
66
|
+
${AGENTS_SNIPPET.join("\n")}
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
No server needed. \`docspack mcp\` serves the same index over MCP for clients that prefer a
|
|
70
|
+
declared tool; both return identical text, scoped to the version this project depends on.
|
|
71
|
+
|
|
72
|
+
Add this as well if you want the agent to record documentation problems it hits:
|
|
73
|
+
|
|
74
|
+
\`\`\`md
|
|
75
|
+
${FEEDBACK_SNIPPET.join("\n")}
|
|
76
|
+
\`\`\`
|
|
77
|
+
|
|
78
|
+
## What is in here
|
|
79
|
+
|
|
80
|
+
\`${LLMS_DIR}/\` holds the machine-readable payload: a manifest and one Markdown file per
|
|
81
|
+
chunk. \`llms.txt\` is the table of contents. Both are generated — see the repository this
|
|
82
|
+
package is built from to change the source documentation.
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function gitignore(): string {
|
|
87
|
+
return `# Generated by \`docspack build\`. Rebuilt on publish by prepublishOnly.
|
|
88
|
+
${LLMS_DIR}/
|
|
89
|
+
llms.txt
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Three seeds that map onto how retrieval works: one chunk should answer one question. The
|
|
95
|
+
* guidance in them is what `docspack doctor` checks, so following it keeps the package clean.
|
|
96
|
+
*/
|
|
97
|
+
export function docSeeds(subject: string): { path: string; contents: string }[] {
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
path: "docs/01-overview.md",
|
|
101
|
+
contents: `# ${subject} overview
|
|
102
|
+
|
|
103
|
+
${PLACEHOLDER} one paragraph on what ${subject} is and the problem it solves. Write plainly:
|
|
104
|
+
no marketing adjectives, no "in this guide". Every sentence should carry a fact, because the
|
|
105
|
+
index matches on the words someone would type.
|
|
106
|
+
|
|
107
|
+
## Mental model
|
|
108
|
+
|
|
109
|
+
${PLACEHOLDER} the two or three concepts a reader needs before anything else makes sense.
|
|
110
|
+
Name them the way your API names them, in \`inline code\` — those names are indexed.
|
|
111
|
+
|
|
112
|
+
## When to use it
|
|
113
|
+
|
|
114
|
+
${PLACEHOLDER} what ${subject} is for, and what it is not for. Keep sentences under 40 words:
|
|
115
|
+
one claim each retrieves better than two joined by a conjunction.
|
|
116
|
+
`,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
path: "docs/02-getting-started.md",
|
|
120
|
+
contents: `# Getting started with ${subject}
|
|
121
|
+
|
|
122
|
+
## Installation
|
|
123
|
+
|
|
124
|
+
${PLACEHOLDER} the install command, and any peer requirement.
|
|
125
|
+
|
|
126
|
+
## Configuration
|
|
127
|
+
|
|
128
|
+
${PLACEHOLDER} the minimum configuration needed to make a first call. Put option names in
|
|
129
|
+
\`inline code\` — docspack indexes those as entities, which is how an agent finds them.
|
|
130
|
+
Show the call rather than describing it; a chunk with no code is hard to act on.
|
|
131
|
+
|
|
132
|
+
## Your first call
|
|
133
|
+
|
|
134
|
+
${PLACEHOLDER} a complete, runnable example. Short and real beats long and abstract.
|
|
135
|
+
`,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
path: "docs/03-api.md",
|
|
139
|
+
contents: `# ${subject} API
|
|
140
|
+
|
|
141
|
+
Give each operation its own \`##\` heading: docspack splits chunks at that level, and one
|
|
142
|
+
chunk that answers one question retrieves far better than one that answers five.
|
|
143
|
+
|
|
144
|
+
## ${PLACEHOLDER} first operation
|
|
145
|
+
|
|
146
|
+
${PLACEHOLDER} what it does, its parameters, what it returns, and one example.
|
|
147
|
+
|
|
148
|
+
## ${PLACEHOLDER} second operation
|
|
149
|
+
|
|
150
|
+
${PLACEHOLDER} same shape as above.
|
|
151
|
+
`,
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function workflow(name: string, buildArgs: string): string {
|
|
157
|
+
return `# Publishes ${name} on every release, so the documentation version always
|
|
158
|
+
# matches the library version. Needs an NPM_TOKEN secret with publish rights.
|
|
159
|
+
|
|
160
|
+
name: Publish documentation package
|
|
161
|
+
|
|
162
|
+
on:
|
|
163
|
+
release:
|
|
164
|
+
types: [published]
|
|
165
|
+
workflow_dispatch:
|
|
166
|
+
inputs:
|
|
167
|
+
version:
|
|
168
|
+
description: Version to publish
|
|
169
|
+
required: true
|
|
170
|
+
|
|
171
|
+
jobs:
|
|
172
|
+
publish:
|
|
173
|
+
runs-on: ubuntu-latest
|
|
174
|
+
permissions:
|
|
175
|
+
contents: read
|
|
176
|
+
id-token: write
|
|
177
|
+
steps:
|
|
178
|
+
- uses: actions/checkout@v5
|
|
179
|
+
|
|
180
|
+
- uses: actions/setup-node@v5
|
|
181
|
+
with:
|
|
182
|
+
node-version: 24
|
|
183
|
+
registry-url: https://registry.npmjs.org
|
|
184
|
+
|
|
185
|
+
- name: Resolve the version
|
|
186
|
+
id: version
|
|
187
|
+
run: echo "value=\${{ inputs.version || github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
|
|
188
|
+
|
|
189
|
+
- name: Generate the documentation package
|
|
190
|
+
run: npx docspack build${buildArgs} --pkg-version "\${{ steps.version.outputs.value }}"
|
|
191
|
+
|
|
192
|
+
- name: Validate before publishing
|
|
193
|
+
run: npx docspack doctor --strict
|
|
194
|
+
|
|
195
|
+
- name: Publish to npm
|
|
196
|
+
run: npm publish --access public --provenance
|
|
197
|
+
env:
|
|
198
|
+
NODE_AUTH_TOKEN: \${{ secrets.NPM_TOKEN }}
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { DocspackError } from "../errors.js";
|
|
4
|
+
import type { InitPlan } from "./plan.js";
|
|
5
|
+
|
|
6
|
+
export type WriteStatus = "created" | "overwritten" | "unchanged" | "skipped";
|
|
7
|
+
|
|
8
|
+
export interface WrittenFile {
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly status: WriteStatus;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ApplyOptions {
|
|
14
|
+
/** Overwrite files that already exist and differ. */
|
|
15
|
+
readonly force?: boolean;
|
|
16
|
+
/** Report what would happen without touching the disk. */
|
|
17
|
+
readonly dryRun?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Writes a plan. Existing files are left alone unless they are identical or `--force` is set, so
|
|
22
|
+
* re-running `init` in a real project never destroys work.
|
|
23
|
+
*/
|
|
24
|
+
export async function applyPlan(
|
|
25
|
+
cwd: string,
|
|
26
|
+
plan: InitPlan,
|
|
27
|
+
options: ApplyOptions = {},
|
|
28
|
+
): Promise<WrittenFile[]> {
|
|
29
|
+
const root = resolve(cwd, plan.dir);
|
|
30
|
+
const results: WrittenFile[] = [];
|
|
31
|
+
|
|
32
|
+
for (const file of plan.files) {
|
|
33
|
+
const target = resolve(root, file.path);
|
|
34
|
+
const inside = relative(root, target);
|
|
35
|
+
if (inside.length === 0 || inside.startsWith("..") || inside.startsWith(`..${sep}`)) {
|
|
36
|
+
throw new DocspackError(`Refusing to write "${file.path}" outside of ${plan.dir}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const existing = await read(target);
|
|
40
|
+
if (existing === file.contents) {
|
|
41
|
+
results.push({ path: file.path, status: "unchanged" });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (existing !== undefined && options.force !== true) {
|
|
45
|
+
results.push({ path: file.path, status: "skipped" });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (options.dryRun !== true) {
|
|
50
|
+
await mkdir(dirname(target), { recursive: true });
|
|
51
|
+
await writeFile(target, file.contents, "utf8");
|
|
52
|
+
}
|
|
53
|
+
results.push({ path: file.path, status: existing === undefined ? "created" : "overwritten" });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Renders a plan as the tree `--dry-run` and the confirmation step print. */
|
|
60
|
+
export function renderTree(plan: InitPlan, written?: readonly WrittenFile[]): string {
|
|
61
|
+
const status = new Map(written?.map((file) => [file.path, file.status]));
|
|
62
|
+
const paths = [...plan.files].map((file) => file.path).sort();
|
|
63
|
+
const lines = [`${plan.dir}/`];
|
|
64
|
+
|
|
65
|
+
paths.forEach((path, index) => {
|
|
66
|
+
const marker = index === paths.length - 1 ? "└──" : "├──";
|
|
67
|
+
const state = status.get(path);
|
|
68
|
+
lines.push(
|
|
69
|
+
`${marker} ${path}${state === undefined || state === "created" ? "" : ` (${state})`}`,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function read(path: string): Promise<string | undefined> {
|
|
77
|
+
try {
|
|
78
|
+
return await readFile(path, "utf8");
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/kinds.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kinds of documentation problem docspack records.
|
|
3
|
+
*
|
|
4
|
+
* `drift` is Tier A: a name the documentation uses that the library does not declare, which is
|
|
5
|
+
* a fact about two files and needs no model to establish. The other two are Tier B, where a
|
|
6
|
+
* reader exercised judgment and therefore has to show its work.
|
|
7
|
+
*
|
|
8
|
+
* There is deliberately no kind for "this page is confusing". Unfalsifiable claims are
|
|
9
|
+
* infinitely generatable and of no use to a maintainer.
|
|
10
|
+
*/
|
|
11
|
+
export const KINDS = ["drift", "incorrect", "missing"] as const;
|
|
12
|
+
|
|
13
|
+
export type FindingKind = (typeof KINDS)[number];
|
|
14
|
+
|
|
15
|
+
export function isFindingKind(value: unknown): value is FindingKind {
|
|
16
|
+
return typeof value === "string" && (KINDS as readonly string[]).includes(value);
|
|
17
|
+
}
|
package/src/llms-txt.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export interface LlmsTxtLink {
|
|
2
|
+
readonly title: string;
|
|
3
|
+
readonly url: string;
|
|
4
|
+
readonly notes?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface LlmsTxtSection {
|
|
8
|
+
readonly heading: string;
|
|
9
|
+
readonly links: readonly LlmsTxtLink[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface LlmsTxtDocument {
|
|
13
|
+
readonly title?: string;
|
|
14
|
+
readonly summary?: string;
|
|
15
|
+
readonly sections: readonly LlmsTxtSection[];
|
|
16
|
+
/** Every link in document order, across all sections. */
|
|
17
|
+
readonly links: readonly LlmsTxtLink[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const H1 = /^#\s+(.+?)\s*$/;
|
|
21
|
+
const H2 = /^##\s+(.+?)\s*$/;
|
|
22
|
+
const QUOTE = /^>\s?(.*)$/;
|
|
23
|
+
const LIST_LINK = /^\s*[-*+]\s+\[([^\]]*)\]\(([^)]+)\)\s*(?::\s*(.*))?$/;
|
|
24
|
+
const FENCE = /^\s*(?:```|~~~)/;
|
|
25
|
+
|
|
26
|
+
/** Links before the first `##` heading land in this section. */
|
|
27
|
+
const PREAMBLE = "";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parses the llms.txt format (https://llmstxt.org). The parser is deliberately tolerant: real
|
|
31
|
+
* files in the wild omit the H1, start at an H2, put the blockquote before the title, and nest
|
|
32
|
+
* link items several levels deep.
|
|
33
|
+
*/
|
|
34
|
+
export function parseLlmsTxt(text: string): LlmsTxtDocument {
|
|
35
|
+
const lines = text.replace(/^/, "").replace(/\r\n/g, "\n").split("\n");
|
|
36
|
+
|
|
37
|
+
let title: string | undefined;
|
|
38
|
+
const summaryLines: string[] = [];
|
|
39
|
+
let summaryClosed = false;
|
|
40
|
+
let inFence = false;
|
|
41
|
+
let heading = PREAMBLE;
|
|
42
|
+
|
|
43
|
+
const sections = new Map<string, LlmsTxtLink[]>();
|
|
44
|
+
const links: LlmsTxtLink[] = [];
|
|
45
|
+
|
|
46
|
+
const push = (link: LlmsTxtLink): void => {
|
|
47
|
+
const bucket = sections.get(heading);
|
|
48
|
+
if (bucket === undefined) sections.set(heading, [link]);
|
|
49
|
+
else bucket.push(link);
|
|
50
|
+
links.push(link);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
if (FENCE.test(line)) {
|
|
55
|
+
inFence = !inFence;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (inFence) continue;
|
|
59
|
+
|
|
60
|
+
const h2 = H2.exec(line);
|
|
61
|
+
if (h2?.[1] !== undefined) {
|
|
62
|
+
heading = h2[1];
|
|
63
|
+
summaryClosed = true;
|
|
64
|
+
if (!sections.has(heading)) sections.set(heading, []);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const h1 = H1.exec(line);
|
|
69
|
+
if (h1?.[1] !== undefined) {
|
|
70
|
+
title ??= h1[1];
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const quote = QUOTE.exec(line);
|
|
75
|
+
if (quote?.[1] !== undefined) {
|
|
76
|
+
if (!summaryClosed) summaryLines.push(quote[1].trim());
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (summaryLines.length > 0 && line.trim().length === 0) summaryClosed = true;
|
|
80
|
+
|
|
81
|
+
const item = LIST_LINK.exec(line);
|
|
82
|
+
if (item?.[1] !== undefined && item[2] !== undefined) {
|
|
83
|
+
const url = item[2].trim().split(/\s+/)[0];
|
|
84
|
+
if (url === undefined || url.length === 0) continue;
|
|
85
|
+
const notes = item[3]?.trim();
|
|
86
|
+
push({
|
|
87
|
+
title: item[1].trim(),
|
|
88
|
+
url,
|
|
89
|
+
...(notes === undefined || notes.length === 0 ? {} : { notes }),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const summary = summaryLines.join(" ").trim();
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
...(title === undefined ? {} : { title }),
|
|
98
|
+
...(summary.length === 0 ? {} : { summary }),
|
|
99
|
+
sections: [...sections].map(([name, sectionLinks]) => ({ heading: name, links: sectionLinks })),
|
|
100
|
+
links,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Absolute http(s) links, de-duplicated, in document order. */
|
|
105
|
+
export function fetchableLinks(document: LlmsTxtDocument): readonly LlmsTxtLink[] {
|
|
106
|
+
const seen = new Set<string>();
|
|
107
|
+
const result: LlmsTxtLink[] = [];
|
|
108
|
+
for (const link of document.links) {
|
|
109
|
+
if (!/^https?:\/\//i.test(link.url)) continue;
|
|
110
|
+
const key = link.url.split("#")[0] ?? link.url;
|
|
111
|
+
if (seen.has(key)) continue;
|
|
112
|
+
seen.add(key);
|
|
113
|
+
result.push(link);
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|