docspack 0.0.1 → 0.1.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/LICENSE +21 -0
- package/README.md +59 -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 +60 -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/cli.ts
ADDED
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { posix } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { defaultStorePath, Store, silenceSqliteWarning } from "./db.js";
|
|
7
|
+
import { discoverPackages } from "./discovery.js";
|
|
8
|
+
import { type DoctorReport, runDoctor } from "./doctor.js";
|
|
9
|
+
import { DocspackError } from "./errors.js";
|
|
10
|
+
import { renderTree } from "./init/write.js";
|
|
11
|
+
import { previewPackage } from "./preview.js";
|
|
12
|
+
import { DEFAULT_LIMIT, DEFAULT_MAX_TOKENS, queryDocs, renderAnswer } from "./search.js";
|
|
13
|
+
import { AGENTS_SNIPPET, FEEDBACK_SNIPPET } from "./snippet.js";
|
|
14
|
+
import { syncProject } from "./sync.js";
|
|
15
|
+
import { verifyProject } from "./verify.js";
|
|
16
|
+
|
|
17
|
+
// The MCP SDK costs ~200ms to load and the Markdown converter ~60ms. Neither is needed to
|
|
18
|
+
// answer a query, so the commands that need them import them at the point of use. Every
|
|
19
|
+
// `docspack ask` pays for what it uses and nothing else.
|
|
20
|
+
|
|
21
|
+
const OPTIONS = {
|
|
22
|
+
help: { type: "boolean", short: "h" },
|
|
23
|
+
version: { type: "boolean", short: "v" },
|
|
24
|
+
json: { type: "boolean" },
|
|
25
|
+
quiet: { type: "boolean", short: "q" },
|
|
26
|
+
cwd: { type: "string" },
|
|
27
|
+
store: { type: "string" },
|
|
28
|
+
force: { type: "boolean" },
|
|
29
|
+
package: { type: "string", short: "p" },
|
|
30
|
+
limit: { type: "string" },
|
|
31
|
+
"max-tokens": { type: "string" },
|
|
32
|
+
all: { type: "boolean" },
|
|
33
|
+
from: { type: "string" },
|
|
34
|
+
openapi: { type: "string" },
|
|
35
|
+
out: { type: "string" },
|
|
36
|
+
name: { type: "string" },
|
|
37
|
+
"pkg-version": { type: "string" },
|
|
38
|
+
pages: { type: "string" },
|
|
39
|
+
mirror: { type: "string" },
|
|
40
|
+
mode: { type: "string" },
|
|
41
|
+
template: { type: "string" },
|
|
42
|
+
community: { type: "boolean" },
|
|
43
|
+
workflow: { type: "boolean" },
|
|
44
|
+
"no-workflow": { type: "boolean" },
|
|
45
|
+
"no-build": { type: "boolean" },
|
|
46
|
+
yes: { type: "boolean", short: "y" },
|
|
47
|
+
"dry-run": { type: "boolean" },
|
|
48
|
+
strict: { type: "boolean" },
|
|
49
|
+
"package-dir": { type: "string" },
|
|
50
|
+
chunk: { type: "string" },
|
|
51
|
+
kind: { type: "string" },
|
|
52
|
+
evidence: { type: "string" },
|
|
53
|
+
expected: { type: "string" },
|
|
54
|
+
actual: { type: "string" },
|
|
55
|
+
repro: { type: "string" },
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
const HELP = `docspack — local, version-locked documentation for AI agents
|
|
59
|
+
|
|
60
|
+
Usage
|
|
61
|
+
docspack <command> [options]
|
|
62
|
+
|
|
63
|
+
Commands
|
|
64
|
+
sync Index the docs packages this project depends on
|
|
65
|
+
ask <question> Answer from the local index — the command to give an agent
|
|
66
|
+
search <query> Same index, formatted for a human reading the terminal
|
|
67
|
+
list Show this project's docs packages and their index state
|
|
68
|
+
verify Check the docs still describe the code you installed
|
|
69
|
+
feedback <sub> Record documentation problems: add, list, submit, remove
|
|
70
|
+
mcp Serve the index over MCP instead, as a long-lived process
|
|
71
|
+
sources List curated sources that \`docspack build\` can fetch
|
|
72
|
+
|
|
73
|
+
Authoring
|
|
74
|
+
init Scaffold a documentation package, then build and check it
|
|
75
|
+
build [source] Generate the .llms/ payload for publishing
|
|
76
|
+
doctor Check a package the way the indexer and a reviewer would
|
|
77
|
+
preview <query> Answer a query from the local package, as an agent would
|
|
78
|
+
|
|
79
|
+
How it works
|
|
80
|
+
Documentation ships as npm packages named @vendor/docspack or
|
|
81
|
+
@docspack-community/<name>. Add them to package.json, run \`docspack sync\`, and
|
|
82
|
+
every chunk is indexed into a shared SQLite database with FTS5. Agents query
|
|
83
|
+
that index locally: no network, no scraping, always the installed version.
|
|
84
|
+
|
|
85
|
+
Giving an agent access
|
|
86
|
+
Any agent with a shell can run \`docspack ask\`, so one line in AGENTS.md or
|
|
87
|
+
CLAUDE.md is the whole setup — no server, no per-agent configuration, and
|
|
88
|
+
nothing resident when nobody is asking:
|
|
89
|
+
|
|
90
|
+
${AGENTS_SNIPPET.map((line) => ` ${line}`).join("\n")}
|
|
91
|
+
|
|
92
|
+
Add the block below as well if you want the agent to record documentation
|
|
93
|
+
problems it runs into. It writes to a local file; nothing is sent:
|
|
94
|
+
|
|
95
|
+
${FEEDBACK_SNIPPET.map((line) => ` ${line}`).join("\n")}
|
|
96
|
+
|
|
97
|
+
\`docspack mcp\` serves the same index over the Model Context Protocol for
|
|
98
|
+
clients that prefer a tool definition. Both return identical text.
|
|
99
|
+
|
|
100
|
+
Recording documentation problems
|
|
101
|
+
\`docspack feedback add\` writes a finding to .docspack/feedback.jsonl in this
|
|
102
|
+
project. Nothing is transmitted: docspack contains no code that can send a
|
|
103
|
+
report anywhere, and it never will.
|
|
104
|
+
|
|
105
|
+
docspack feedback add --chunk @acme/docspack@1.4.0/api-auth \\
|
|
106
|
+
--kind drift --evidence "client.setKey is not exported; setApiKey is"
|
|
107
|
+
|
|
108
|
+
A finding has to be falsifiable. \`drift\` must name the identifier that
|
|
109
|
+
drifted. \`incorrect\` and \`missing\` must carry --expected, --actual and
|
|
110
|
+
--repro, so a claim that cannot show its work cannot be recorded at all.
|
|
111
|
+
Recording the same problem again increments a counter instead of adding a
|
|
112
|
+
second entry.
|
|
113
|
+
|
|
114
|
+
\`docspack feedback submit\` prints a prefilled GitHub issue URL for every
|
|
115
|
+
finding whose vendor asked to receive it, and routes nothing anywhere else.
|
|
116
|
+
A vendor opts in from their own package.json:
|
|
117
|
+
|
|
118
|
+
"docspack": {
|
|
119
|
+
"feedback": {
|
|
120
|
+
"github": "acme/sdk",
|
|
121
|
+
"labels": ["documentation"],
|
|
122
|
+
"accepts": ["drift"]
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
No \`feedback\` block means no channel and no route. \`accepts\` narrows which
|
|
127
|
+
kinds reach them. A human opens the link, reads it, and files it.
|
|
128
|
+
|
|
129
|
+
Options
|
|
130
|
+
-y, --yes init: skip prompts and use flags plus detected defaults
|
|
131
|
+
--dry-run init: print the file tree and write nothing
|
|
132
|
+
--mirror <id|url> init: bootstrap from a published llms.txt
|
|
133
|
+
--community init: scaffold under @docspack-community
|
|
134
|
+
--no-workflow init: skip the release workflow
|
|
135
|
+
--no-build init: scaffold only
|
|
136
|
+
--template <t> init: full (default) or minimal
|
|
137
|
+
--strict doctor: treat warnings as failures
|
|
138
|
+
--package-dir <d> doctor, preview, verify: the package to inspect (default: .)
|
|
139
|
+
--chunk <id> feedback add: the chunk the problem is in
|
|
140
|
+
--kind <k> feedback add: drift, incorrect or missing
|
|
141
|
+
--evidence <text> feedback add: the claim, in one line
|
|
142
|
+
--expected <text> feedback add: what the documentation led you to expect
|
|
143
|
+
--actual <text> feedback add: what happened instead
|
|
144
|
+
--repro <code> feedback add: code that demonstrates it
|
|
145
|
+
--force sync: re-index packages already in the store; init: overwrite files
|
|
146
|
+
-p, --package <s> search: only packages whose name contains this text
|
|
147
|
+
--limit <n> search: maximum chunks to return (default ${DEFAULT_LIMIT})
|
|
148
|
+
--max-tokens <n> search: token ceiling for the result set (default ${DEFAULT_MAX_TOKENS})
|
|
149
|
+
--all search: the whole store, not just this project;
|
|
150
|
+
feedback remove: every recorded finding
|
|
151
|
+
--from <dir> build: directory of Markdown to package
|
|
152
|
+
--openapi <file> build: OpenAPI JSON to package, one chunk per operation
|
|
153
|
+
--name <name> build: package name, e.g. @acme/docspack
|
|
154
|
+
--pkg-version <v> build: package version
|
|
155
|
+
--out <dir> build: output directory (default: the current directory)
|
|
156
|
+
--pages <n> build: maximum documents to fetch from a remote source
|
|
157
|
+
--store <path> Use a different index (default: ${defaultStorePath()})
|
|
158
|
+
--cwd <dir> Run in a different directory
|
|
159
|
+
--json Machine-readable output
|
|
160
|
+
-q, --quiet Only print errors
|
|
161
|
+
-h, --help Show this help
|
|
162
|
+
-v, --version Show the version
|
|
163
|
+
|
|
164
|
+
Examples
|
|
165
|
+
npx docspack sync
|
|
166
|
+
npx docspack ask "how do I verify a webhook signature"
|
|
167
|
+
npx docspack search "webhook signature" --package stripe
|
|
168
|
+
npx docspack init
|
|
169
|
+
npx docspack init --name @acme/docspack --from ./docs --yes
|
|
170
|
+
npx docspack doctor --strict
|
|
171
|
+
npx docspack preview "how do I authenticate"
|
|
172
|
+
npx docspack verify
|
|
173
|
+
npx docspack feedback list
|
|
174
|
+
npx docspack feedback submit
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
type Values = {
|
|
178
|
+
[K in keyof typeof OPTIONS]?: (typeof OPTIONS)[K]["type"] extends "boolean" ? boolean : string;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const color = process.env.NO_COLOR === undefined && process.stdout.isTTY === true;
|
|
182
|
+
const paint = (code: string, text: string): string =>
|
|
183
|
+
color ? `\u001b[${code}m${text}\u001b[0m` : text;
|
|
184
|
+
const bold = (text: string): string => paint("1", text);
|
|
185
|
+
const dim = (text: string): string => paint("2", text);
|
|
186
|
+
const green = (text: string): string => paint("32", text);
|
|
187
|
+
const yellow = (text: string): string => paint("33", text);
|
|
188
|
+
const red = (text: string): string => paint("31", text);
|
|
189
|
+
|
|
190
|
+
function version(): string {
|
|
191
|
+
const url = new URL("../package.json", import.meta.url);
|
|
192
|
+
const parsed: unknown = JSON.parse(readFileSync(fileURLToPath(url), "utf8"));
|
|
193
|
+
const value = (parsed as { version?: unknown }).version;
|
|
194
|
+
return typeof value === "string" ? value : "0.0.0";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function integer(raw: string | undefined, flag: string): number | undefined {
|
|
198
|
+
if (raw === undefined) return undefined;
|
|
199
|
+
const value = Number(raw);
|
|
200
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
201
|
+
throw new DocspackError(`${flag} expects a non-negative integer, got "${raw}"`);
|
|
202
|
+
}
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function plural(count: number, noun: string): string {
|
|
207
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** First few lines of prose from a chunk, skipping blanks and provenance comments. */
|
|
211
|
+
function preview(content: string, lines: number): string[] {
|
|
212
|
+
return content
|
|
213
|
+
.split("\n")
|
|
214
|
+
.filter((line) => line.trim().length > 0 && !line.trimStart().startsWith("<!--"))
|
|
215
|
+
.slice(0, lines);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function reportDoctor(report: DoctorReport, quiet: boolean): void {
|
|
219
|
+
for (const finding of report.findings) {
|
|
220
|
+
const mark =
|
|
221
|
+
finding.severity === "error"
|
|
222
|
+
? red("error")
|
|
223
|
+
: finding.severity === "warn"
|
|
224
|
+
? yellow("warn")
|
|
225
|
+
: dim("info");
|
|
226
|
+
const where = finding.where === undefined ? "" : dim(` ${finding.where}`);
|
|
227
|
+
process.stderr.write(`${mark} ${finding.message}${where}\n`);
|
|
228
|
+
if (finding.fix !== undefined) process.stderr.write(` ${dim(finding.fix)}\n`);
|
|
229
|
+
}
|
|
230
|
+
if (quiet) return;
|
|
231
|
+
|
|
232
|
+
if (report.ok) {
|
|
233
|
+
const notes = report.findings.length > 0 ? ` (${plural(report.findings.length, "note")})` : "";
|
|
234
|
+
process.stdout.write(
|
|
235
|
+
`${green("ok")} ${plural(report.chunks, "chunk")}, ~${report.tokens} tokens${notes}\n`,
|
|
236
|
+
);
|
|
237
|
+
} else {
|
|
238
|
+
process.stderr.write(`${red("failed")} the package is not ready to publish\n`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function openStore(values: Values): Store {
|
|
243
|
+
return Store.open(values.store ?? defaultStorePath());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function main(argv: readonly string[]): Promise<number> {
|
|
247
|
+
let values: Values;
|
|
248
|
+
let positionals: string[];
|
|
249
|
+
try {
|
|
250
|
+
const parsed = parseArgs({ args: [...argv], options: OPTIONS, allowPositionals: true });
|
|
251
|
+
values = parsed.values;
|
|
252
|
+
positionals = parsed.positionals;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
process.stderr.write(
|
|
255
|
+
`${red("error")} ${error instanceof Error ? error.message : String(error)}\n\n`,
|
|
256
|
+
);
|
|
257
|
+
process.stderr.write(HELP);
|
|
258
|
+
return 2;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (values.version === true) {
|
|
262
|
+
process.stdout.write(`${version()}\n`);
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const [command = "help", ...rest] = positionals;
|
|
267
|
+
if (values.help === true || command === "help") {
|
|
268
|
+
process.stdout.write(HELP);
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const cwd = values.cwd ?? process.cwd();
|
|
273
|
+
const quiet = values.quiet === true;
|
|
274
|
+
const json = values.json === true;
|
|
275
|
+
|
|
276
|
+
switch (command) {
|
|
277
|
+
case "sync": {
|
|
278
|
+
const store = openStore(values);
|
|
279
|
+
try {
|
|
280
|
+
const result = await syncProject({
|
|
281
|
+
cwd,
|
|
282
|
+
store,
|
|
283
|
+
...(values.force === true ? { force: true } : {}),
|
|
284
|
+
...(quiet || json
|
|
285
|
+
? {}
|
|
286
|
+
: {
|
|
287
|
+
onProgress: (message: string): void => {
|
|
288
|
+
process.stderr.write(`${dim(message)}\n`);
|
|
289
|
+
},
|
|
290
|
+
}),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (json) {
|
|
294
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
295
|
+
return result.packages.length === 0 && result.problems.length > 0 ? 1 : 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const pkg of result.packages) {
|
|
299
|
+
const mark = pkg.status === "indexed" ? green("+") : dim("=");
|
|
300
|
+
const trust = pkg.trusted ? "" : ` ${yellow("(community)")}`;
|
|
301
|
+
process.stdout.write(
|
|
302
|
+
`${mark} ${bold(pkg.id)} ${pkg.chunks} chunks ${dim(pkg.status)}${trust}\n`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
for (const problem of result.problems) {
|
|
306
|
+
process.stderr.write(`${yellow("!")} ${problem}\n`);
|
|
307
|
+
}
|
|
308
|
+
if (result.packages.length === 0 && !quiet) {
|
|
309
|
+
process.stdout.write(
|
|
310
|
+
"No documentation packages found. Add one, for example:\n pnpm add -D @docspack-community/<name>\n",
|
|
311
|
+
);
|
|
312
|
+
} else if (!quiet) {
|
|
313
|
+
process.stdout.write(
|
|
314
|
+
[
|
|
315
|
+
"",
|
|
316
|
+
"Give an agent access with one line in AGENTS.md or CLAUDE.md:",
|
|
317
|
+
...AGENTS_SNIPPET.map((line) => dim(` ${line}`)),
|
|
318
|
+
"",
|
|
319
|
+
"Add this too if you want the agent to record documentation problems:",
|
|
320
|
+
...FEEDBACK_SNIPPET.map((line) => dim(` ${line}`)),
|
|
321
|
+
"",
|
|
322
|
+
].join("\n"),
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
return 0;
|
|
326
|
+
} finally {
|
|
327
|
+
store.close();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
case "ask": {
|
|
332
|
+
const question = rest.join(" ");
|
|
333
|
+
if (question.length === 0) {
|
|
334
|
+
throw new DocspackError('Usage: docspack ask "<question>"');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const store = openStore(values);
|
|
338
|
+
try {
|
|
339
|
+
const result = await queryDocs({
|
|
340
|
+
cwd,
|
|
341
|
+
store,
|
|
342
|
+
query: question,
|
|
343
|
+
...(values.package === undefined ? {} : { packageFilter: values.package }),
|
|
344
|
+
...(() => {
|
|
345
|
+
const limit = integer(values.limit, "--limit");
|
|
346
|
+
return limit === undefined ? {} : { limit };
|
|
347
|
+
})(),
|
|
348
|
+
...(() => {
|
|
349
|
+
const maxTokens = integer(values["max-tokens"], "--max-tokens");
|
|
350
|
+
return maxTokens === undefined ? {} : { maxTokens };
|
|
351
|
+
})(),
|
|
352
|
+
...(values.all === true ? { scoped: false } : {}),
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
if (json) {
|
|
356
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
357
|
+
} else {
|
|
358
|
+
// Exactly what the MCP tool returns, so both interfaces answer identically.
|
|
359
|
+
process.stdout.write(`${renderAnswer(result, question)}\n`);
|
|
360
|
+
}
|
|
361
|
+
// Answering "nothing matched" is a successful answer, not a failed command.
|
|
362
|
+
return 0;
|
|
363
|
+
} finally {
|
|
364
|
+
store.close();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
case "search": {
|
|
369
|
+
const query = rest.join(" ");
|
|
370
|
+
if (query.length === 0) throw new DocspackError("Usage: docspack search <query>");
|
|
371
|
+
|
|
372
|
+
const store = openStore(values);
|
|
373
|
+
try {
|
|
374
|
+
const result = await queryDocs({
|
|
375
|
+
cwd,
|
|
376
|
+
store,
|
|
377
|
+
query,
|
|
378
|
+
...(values.package === undefined ? {} : { packageFilter: values.package }),
|
|
379
|
+
...(() => {
|
|
380
|
+
const limit = integer(values.limit, "--limit");
|
|
381
|
+
return limit === undefined ? {} : { limit };
|
|
382
|
+
})(),
|
|
383
|
+
...(() => {
|
|
384
|
+
const maxTokens = integer(values["max-tokens"], "--max-tokens");
|
|
385
|
+
return maxTokens === undefined ? {} : { maxTokens };
|
|
386
|
+
})(),
|
|
387
|
+
...(values.all === true ? { scoped: false } : {}),
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
if (json) {
|
|
391
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
if (result.hits.length === 0) {
|
|
395
|
+
process.stdout.write(`No local documentation matched "${query}".\n`);
|
|
396
|
+
return 1;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
for (const hit of result.hits) {
|
|
400
|
+
const trust = hit.trusted ? "" : ` ${yellow("(community)")}`;
|
|
401
|
+
process.stdout.write(
|
|
402
|
+
`${bold(hit.chunkId)}${trust} ${dim(`${hit.tokens} tokens · ${hit.filePath}`)}\n`,
|
|
403
|
+
);
|
|
404
|
+
for (const line of preview(hit.content, 4)) {
|
|
405
|
+
process.stdout.write(` ${line}\n`);
|
|
406
|
+
}
|
|
407
|
+
process.stdout.write("\n");
|
|
408
|
+
}
|
|
409
|
+
process.stdout.write(
|
|
410
|
+
dim(`${result.tokens} tokens across ${plural(result.hits.length, "chunk")}\n`),
|
|
411
|
+
);
|
|
412
|
+
return 0;
|
|
413
|
+
} finally {
|
|
414
|
+
store.close();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
case "list": {
|
|
419
|
+
const store = openStore(values);
|
|
420
|
+
try {
|
|
421
|
+
const { packages, problems } = await discoverPackages(cwd);
|
|
422
|
+
const rows = packages.map((pkg) => ({
|
|
423
|
+
id: pkg.id,
|
|
424
|
+
name: pkg.name,
|
|
425
|
+
version: pkg.version,
|
|
426
|
+
chunks: pkg.manifest.chunks.length,
|
|
427
|
+
trusted: pkg.trusted,
|
|
428
|
+
indexed: store.hasPackage(pkg.id),
|
|
429
|
+
}));
|
|
430
|
+
|
|
431
|
+
if (json) {
|
|
432
|
+
process.stdout.write(`${JSON.stringify({ packages: rows, problems }, null, 2)}\n`);
|
|
433
|
+
return 0;
|
|
434
|
+
}
|
|
435
|
+
if (rows.length === 0) {
|
|
436
|
+
process.stdout.write("This project has no documentation packages.\n");
|
|
437
|
+
}
|
|
438
|
+
for (const row of rows) {
|
|
439
|
+
const state = row.indexed ? green("indexed") : yellow("not indexed");
|
|
440
|
+
const trust = row.trusted ? "" : ` ${yellow("(community)")}`;
|
|
441
|
+
process.stdout.write(`${bold(row.id)} ${row.chunks} chunks ${state}${trust}\n`);
|
|
442
|
+
}
|
|
443
|
+
for (const problem of problems) process.stderr.write(`${yellow("!")} ${problem}\n`);
|
|
444
|
+
return 0;
|
|
445
|
+
} finally {
|
|
446
|
+
store.close();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
case "verify": {
|
|
451
|
+
const report = await verifyProject({
|
|
452
|
+
cwd,
|
|
453
|
+
...(values["package-dir"] === undefined ? {} : { packageDir: values["package-dir"] }),
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
if (json) {
|
|
457
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
458
|
+
return report.ok ? 0 : 1;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (report.packages.length === 0) {
|
|
462
|
+
process.stdout.write("This project has no documentation packages.\n");
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
for (const pkg of report.packages) {
|
|
467
|
+
if (pkg.status === "skipped") {
|
|
468
|
+
if (!quiet)
|
|
469
|
+
process.stdout.write(`${dim("-")} ${bold(pkg.id)} ${dim(pkg.reason ?? "")}\n`);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const mark = pkg.findings.length === 0 ? green("ok") : red("drift");
|
|
474
|
+
process.stdout.write(
|
|
475
|
+
`${mark} ${bold(pkg.id)} ${dim(`documents ${pkg.documents ?? ""} · ${plural(pkg.checked, "name")} checked, ${pkg.matched} declared`)}\n`,
|
|
476
|
+
);
|
|
477
|
+
for (const finding of pkg.findings) {
|
|
478
|
+
process.stderr.write(
|
|
479
|
+
` ${yellow(finding.entity)} is not declared; did the API become ${bold(finding.suggestion)}?\n`,
|
|
480
|
+
);
|
|
481
|
+
process.stderr.write(` ${dim(`${finding.chunkId} · ${finding.file}`)}\n`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!report.ok && !quiet) {
|
|
486
|
+
process.stderr.write(
|
|
487
|
+
`\n${dim("These are local findings. Fix the documentation, or report them to the\nmaintainer yourself — docspack never sends anything anywhere.")}\n`,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
return report.ok ? 0 : 1;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
case "feedback": {
|
|
494
|
+
const { addFinding, feedbackPath, readFeedback, removeFindings } = await import(
|
|
495
|
+
"./feedback.js"
|
|
496
|
+
);
|
|
497
|
+
const [sub = "list", ...args] = rest;
|
|
498
|
+
|
|
499
|
+
switch (sub) {
|
|
500
|
+
case "add": {
|
|
501
|
+
if (values.chunk === undefined || values.evidence === undefined) {
|
|
502
|
+
throw new DocspackError(
|
|
503
|
+
'Usage: docspack feedback add --chunk <id> --kind <k> --evidence "<claim>"',
|
|
504
|
+
{ hint: "See `docspack --help` for what each kind requires." },
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const { finding, repeat } = await addFinding({
|
|
509
|
+
cwd,
|
|
510
|
+
chunkId: values.chunk,
|
|
511
|
+
kind: values.kind ?? "drift",
|
|
512
|
+
evidence: values.evidence,
|
|
513
|
+
...(values.expected === undefined ? {} : { expected: values.expected }),
|
|
514
|
+
...(values.actual === undefined ? {} : { actual: values.actual }),
|
|
515
|
+
...(values.repro === undefined ? {} : { repro: values.repro }),
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
if (json) {
|
|
519
|
+
process.stdout.write(`${JSON.stringify({ finding, repeat }, null, 2)}\n`);
|
|
520
|
+
return 0;
|
|
521
|
+
}
|
|
522
|
+
if (!quiet) {
|
|
523
|
+
process.stdout.write(
|
|
524
|
+
repeat
|
|
525
|
+
? `${dim("=")} ${bold(finding.fingerprint)} already recorded, now ${finding.seen}×\n`
|
|
526
|
+
: `${green("+")} ${bold(finding.fingerprint)} recorded in ${feedbackPath(cwd)}\n`,
|
|
527
|
+
);
|
|
528
|
+
process.stdout.write(dim("Nothing was sent. Review with `docspack feedback list`.\n"));
|
|
529
|
+
}
|
|
530
|
+
return 0;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
case "list": {
|
|
534
|
+
const findings = await readFeedback(cwd);
|
|
535
|
+
|
|
536
|
+
if (json) {
|
|
537
|
+
process.stdout.write(`${JSON.stringify(findings, null, 2)}\n`);
|
|
538
|
+
return 0;
|
|
539
|
+
}
|
|
540
|
+
if (findings.length === 0) {
|
|
541
|
+
process.stdout.write("No documentation problems recorded in this project.\n");
|
|
542
|
+
return 0;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
for (const finding of findings) {
|
|
546
|
+
const repeats = finding.seen > 1 ? ` ${yellow(`seen ${finding.seen}×`)}` : "";
|
|
547
|
+
process.stdout.write(
|
|
548
|
+
`${bold(finding.fingerprint)} ${finding.kind} ${dim(finding.chunkId)}${repeats}\n`,
|
|
549
|
+
);
|
|
550
|
+
process.stdout.write(` ${finding.evidence}\n`);
|
|
551
|
+
if (finding.repro !== undefined) {
|
|
552
|
+
process.stdout.write(` ${dim(`repro: ${finding.repro.split("\n")[0] ?? ""}`)}\n`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (!quiet) {
|
|
556
|
+
process.stdout.write(
|
|
557
|
+
dim(
|
|
558
|
+
`\n${plural(findings.length, "finding")} in ${feedbackPath(cwd)}, none of it sent anywhere.\n`,
|
|
559
|
+
),
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
return 0;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
case "submit": {
|
|
566
|
+
const { prepareSubmission } = await import("./submit.js");
|
|
567
|
+
const report = await prepareSubmission({
|
|
568
|
+
cwd,
|
|
569
|
+
docspackVersion: version(),
|
|
570
|
+
...(values.package === undefined ? {} : { packageFilter: values.package }),
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
if (json) {
|
|
574
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
575
|
+
return 0;
|
|
576
|
+
}
|
|
577
|
+
if (report.routed.length === 0 && report.blocked.length === 0) {
|
|
578
|
+
process.stdout.write("No documentation problems recorded in this project.\n");
|
|
579
|
+
return 0;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
for (const { packageId: id, channel } of report.channels) {
|
|
583
|
+
process.stdout.write(
|
|
584
|
+
`${bold(id)} accepts documentation problems at ${channel.github}\n`,
|
|
585
|
+
);
|
|
586
|
+
if (channel.policy !== undefined) {
|
|
587
|
+
process.stdout.write(` ${yellow("Read first:")} ${channel.policy}\n`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
for (const finding of report.routed) {
|
|
592
|
+
process.stdout.write(`\n${dim("─".repeat(72))}\n`);
|
|
593
|
+
process.stdout.write(`${bold(finding.fingerprint)} ${dim(finding.chunkId)}\n\n`);
|
|
594
|
+
process.stdout.write(`${finding.title}\n\n${finding.body}\n\n`);
|
|
595
|
+
if (finding.labels.length > 0) {
|
|
596
|
+
process.stdout.write(dim(`Labels: ${finding.labels.join(", ")}\n`));
|
|
597
|
+
}
|
|
598
|
+
if (finding.containsCode) {
|
|
599
|
+
process.stdout.write(
|
|
600
|
+
`${yellow("!")} This report quotes your reproduction. Check it for anything private before filing.\n`,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
process.stdout.write(
|
|
604
|
+
finding.url === undefined
|
|
605
|
+
? dim("Too long for a prefilled link — open a blank issue and paste the above.\n")
|
|
606
|
+
: `${green("Open:")} ${finding.url}\n`,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (report.blocked.length > 0) {
|
|
611
|
+
process.stdout.write(`\n${bold("Not routed")}\n`);
|
|
612
|
+
for (const finding of report.blocked) {
|
|
613
|
+
const why =
|
|
614
|
+
finding.reason === "no-channel"
|
|
615
|
+
? "the package declares no feedback channel"
|
|
616
|
+
: `the vendor accepts only: ${(finding.accepts ?? []).join(", ")}`;
|
|
617
|
+
process.stdout.write(
|
|
618
|
+
` ${finding.fingerprint} ${finding.kind} ${dim(finding.chunkId)}\n ${dim(why)}\n`,
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (!quiet) {
|
|
624
|
+
process.stdout.write(
|
|
625
|
+
[
|
|
626
|
+
"",
|
|
627
|
+
dim("Nothing has been sent. docspack has no code that can send a report. Open a"),
|
|
628
|
+
dim(
|
|
629
|
+
"link, read what is prefilled, edit it, and file it under your own name — then",
|
|
630
|
+
),
|
|
631
|
+
dim("`docspack feedback remove <fingerprint>` to clear it from this list."),
|
|
632
|
+
"",
|
|
633
|
+
].join("\n"),
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
return 0;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
case "remove": {
|
|
640
|
+
const removed = await removeFindings({
|
|
641
|
+
cwd,
|
|
642
|
+
...(values.all === true ? { all: true } : { fingerprints: args }),
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
if (json) {
|
|
646
|
+
process.stdout.write(`${JSON.stringify({ removed }, null, 2)}\n`);
|
|
647
|
+
return 0;
|
|
648
|
+
}
|
|
649
|
+
process.stdout.write(`${green("-")} removed ${plural(removed, "finding")}\n`);
|
|
650
|
+
return 0;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
default:
|
|
654
|
+
throw new DocspackError(`Unknown feedback subcommand "${sub}"`, {
|
|
655
|
+
hint: "Use add, list, submit or remove.",
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
case "mcp": {
|
|
661
|
+
// stdout is the protocol channel from here on.
|
|
662
|
+
const { startMcpServer } = await import("./mcp.js");
|
|
663
|
+
const store = openStore(values);
|
|
664
|
+
await startMcpServer({
|
|
665
|
+
cwd,
|
|
666
|
+
store,
|
|
667
|
+
version: version(),
|
|
668
|
+
storePath: values.store ?? defaultStorePath(),
|
|
669
|
+
});
|
|
670
|
+
return 0;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
case "build": {
|
|
674
|
+
const { buildPackage } = await import("./build.js");
|
|
675
|
+
const result = await buildPackage({
|
|
676
|
+
out: values.out ?? cwd,
|
|
677
|
+
...(rest[0] === undefined ? {} : { source: rest[0] }),
|
|
678
|
+
...(values.from === undefined ? {} : { from: values.from }),
|
|
679
|
+
...(values.openapi === undefined ? {} : { openapi: values.openapi }),
|
|
680
|
+
...(values.name === undefined ? {} : { name: values.name }),
|
|
681
|
+
...(values["pkg-version"] === undefined ? {} : { version: values["pkg-version"] }),
|
|
682
|
+
...(() => {
|
|
683
|
+
const pages = integer(values.pages, "--pages");
|
|
684
|
+
return pages === undefined ? {} : { pages };
|
|
685
|
+
})(),
|
|
686
|
+
...(quiet || json
|
|
687
|
+
? {}
|
|
688
|
+
: {
|
|
689
|
+
onProgress: (message: string): void => {
|
|
690
|
+
process.stderr.write(`${dim(message)}\n`);
|
|
691
|
+
},
|
|
692
|
+
}),
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
if (json) {
|
|
696
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
697
|
+
return 0;
|
|
698
|
+
}
|
|
699
|
+
process.stdout.write(
|
|
700
|
+
`${green("+")} ${bold(`${result.name}@${result.version}`)} ${result.chunks} chunks ${dim(`~${result.tokens} tokens`)}\n`,
|
|
701
|
+
);
|
|
702
|
+
for (const warning of result.warnings) process.stderr.write(`${yellow("!")} ${warning}\n`);
|
|
703
|
+
if (!quiet) {
|
|
704
|
+
process.stdout.write(
|
|
705
|
+
`\nWrote ${result.dir}/.llms and ${result.dir}/llms.txt.\nPublish with \`npm publish\`, then add it to a project and run \`docspack sync\`.\n`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
return 0;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
case "init": {
|
|
712
|
+
const { runInit } = await import("./init/run.js");
|
|
713
|
+
const result = await runInit({
|
|
714
|
+
cwd,
|
|
715
|
+
docspackVersion: version(),
|
|
716
|
+
...(rest[0] === undefined ? {} : { name: rest[0] }),
|
|
717
|
+
...(values.name === undefined ? {} : { name: values.name }),
|
|
718
|
+
...(values["pkg-version"] === undefined ? {} : { version: values["pkg-version"] }),
|
|
719
|
+
...(values.out === undefined ? {} : { out: values.out }),
|
|
720
|
+
...(values.from === undefined ? {} : { from: values.from }),
|
|
721
|
+
...(values.openapi === undefined ? {} : { openapi: values.openapi }),
|
|
722
|
+
...(values.mirror === undefined ? {} : { mirror: values.mirror }),
|
|
723
|
+
...(values.mode === undefined ? {} : { mode: values.mode as "standalone" | "in-repo" }),
|
|
724
|
+
...(values.template === undefined
|
|
725
|
+
? {}
|
|
726
|
+
: { template: values.template as "full" | "minimal" }),
|
|
727
|
+
...(values.community === true ? { community: true } : {}),
|
|
728
|
+
workflow: values["no-workflow"] === true ? false : values.workflow !== false,
|
|
729
|
+
...(values.yes === true || json ? { yes: true } : {}),
|
|
730
|
+
...(values["dry-run"] === true ? { dryRun: true } : {}),
|
|
731
|
+
...(values.force === true ? { force: true } : {}),
|
|
732
|
+
...(values["no-build"] === true ? { build: false } : {}),
|
|
733
|
+
...(quiet || json
|
|
734
|
+
? {}
|
|
735
|
+
: {
|
|
736
|
+
onProgress: (message: string): void => {
|
|
737
|
+
process.stderr.write(`${dim(message)}\n`);
|
|
738
|
+
},
|
|
739
|
+
}),
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
if (json) {
|
|
743
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
744
|
+
return result.cancelled ? 1 : 0;
|
|
745
|
+
}
|
|
746
|
+
if (result.cancelled) {
|
|
747
|
+
process.stdout.write("Nothing was written.\n");
|
|
748
|
+
return 1;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
process.stdout.write(`${renderTree(result.plan, result.written)}\n`);
|
|
752
|
+
const skipped = result.written.filter((file) => file.status === "skipped");
|
|
753
|
+
if (skipped.length > 0) {
|
|
754
|
+
process.stderr.write(
|
|
755
|
+
`${yellow("!")} ${skipped.length} file(s) already existed and were kept. Re-run with --force to replace them.\n`,
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
if (values["dry-run"] === true) {
|
|
759
|
+
process.stdout.write(dim("\n--dry-run: nothing was written.\n"));
|
|
760
|
+
return 0;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (result.build !== undefined) {
|
|
764
|
+
process.stdout.write(
|
|
765
|
+
`\n${green("+")} ${bold(`${result.build.name}@${result.build.version}`)} ${result.build.chunks} chunks ${dim(`~${result.build.tokens} tokens`)}\n`,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
if (result.doctor !== undefined) reportDoctor(result.doctor, quiet);
|
|
769
|
+
|
|
770
|
+
const source =
|
|
771
|
+
result.plan.input.kind === "seeded"
|
|
772
|
+
? `${result.plan.dir}/docs/`
|
|
773
|
+
: result.plan.input.kind === "mirror"
|
|
774
|
+
? result.plan.input.value
|
|
775
|
+
: posix.normalize(posix.join(result.plan.dir, result.plan.input.value));
|
|
776
|
+
|
|
777
|
+
process.stdout.write(
|
|
778
|
+
[
|
|
779
|
+
"",
|
|
780
|
+
"Next:",
|
|
781
|
+
` $EDITOR ${source}${" ".repeat(Math.max(1, 24 - source.length))}write the documentation`,
|
|
782
|
+
` cd ${result.plan.dir} && npx docspack build regenerate the payload`,
|
|
783
|
+
' npx docspack preview "…" see what an agent would receive',
|
|
784
|
+
" npm publish when doctor is happy",
|
|
785
|
+
"",
|
|
786
|
+
].join("\n"),
|
|
787
|
+
);
|
|
788
|
+
return 0;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
case "doctor": {
|
|
792
|
+
const dir = values["package-dir"] ?? cwd;
|
|
793
|
+
const report = await runDoctor({
|
|
794
|
+
dir,
|
|
795
|
+
...(values.strict === true ? { strict: true } : {}),
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
if (json) {
|
|
799
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
800
|
+
return report.ok ? 0 : 1;
|
|
801
|
+
}
|
|
802
|
+
reportDoctor(report, quiet);
|
|
803
|
+
return report.ok ? 0 : 1;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
case "preview": {
|
|
807
|
+
const query = rest.join(" ");
|
|
808
|
+
if (query.length === 0) throw new DocspackError("Usage: docspack preview <query>");
|
|
809
|
+
|
|
810
|
+
const result = await previewPackage({
|
|
811
|
+
dir: values["package-dir"] ?? cwd,
|
|
812
|
+
query,
|
|
813
|
+
...(() => {
|
|
814
|
+
const limit = integer(values.limit, "--limit");
|
|
815
|
+
return limit === undefined ? {} : { limit };
|
|
816
|
+
})(),
|
|
817
|
+
...(() => {
|
|
818
|
+
const maxTokens = integer(values["max-tokens"], "--max-tokens");
|
|
819
|
+
return maxTokens === undefined ? {} : { maxTokens };
|
|
820
|
+
})(),
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
if (json) {
|
|
824
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
825
|
+
return 0;
|
|
826
|
+
}
|
|
827
|
+
if (result.hits.length === 0) {
|
|
828
|
+
process.stdout.write(
|
|
829
|
+
`Nothing in this package matched "${query}" (${result.indexed} chunks indexed).\n`,
|
|
830
|
+
);
|
|
831
|
+
return 1;
|
|
832
|
+
}
|
|
833
|
+
for (const hit of result.hits) {
|
|
834
|
+
process.stdout.write(
|
|
835
|
+
`${bold(hit.chunkId)} ${dim(`${hit.tokens} tokens · ${hit.filePath}`)}\n`,
|
|
836
|
+
);
|
|
837
|
+
for (const line of preview(hit.content, 4)) process.stdout.write(` ${line}\n`);
|
|
838
|
+
process.stdout.write("\n");
|
|
839
|
+
}
|
|
840
|
+
process.stdout.write(
|
|
841
|
+
dim(
|
|
842
|
+
`${result.tokens} tokens across ${plural(result.hits.length, "chunk")} — what an agent would receive\n`,
|
|
843
|
+
),
|
|
844
|
+
);
|
|
845
|
+
return 0;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
case "sources": {
|
|
849
|
+
const { entries: registryEntries } = await import("@docspack/registry");
|
|
850
|
+
if (json) {
|
|
851
|
+
process.stdout.write(`${JSON.stringify(registryEntries, null, 2)}\n`);
|
|
852
|
+
return 0;
|
|
853
|
+
}
|
|
854
|
+
for (const entry of registryEntries) {
|
|
855
|
+
process.stdout.write(`${bold(entry.id)} ${entry.name}\n ${dim(entry.description)}\n`);
|
|
856
|
+
}
|
|
857
|
+
return 0;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
default:
|
|
861
|
+
process.stderr.write(`${red("error")} unknown command "${command}"\n\n`);
|
|
862
|
+
process.stderr.write(HELP);
|
|
863
|
+
return 2;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
silenceSqliteWarning();
|
|
868
|
+
|
|
869
|
+
main(process.argv.slice(2))
|
|
870
|
+
.then((code) => {
|
|
871
|
+
process.exitCode = code;
|
|
872
|
+
})
|
|
873
|
+
.catch((error: unknown) => {
|
|
874
|
+
if (error instanceof DocspackError) {
|
|
875
|
+
process.stderr.write(`${red("error")} ${error.message}\n`);
|
|
876
|
+
if (error.hint !== undefined) process.stderr.write(`${error.hint}\n`);
|
|
877
|
+
} else {
|
|
878
|
+
process.stderr.write(
|
|
879
|
+
`${red("error")} ${error instanceof Error ? error.stack : String(error)}\n`,
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
process.exitCode = 1;
|
|
883
|
+
});
|