docspack 1.1.0 → 1.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/README.md +25 -1
- package/dist/build.d.ts +5 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +68 -5
- package/dist/build.js.map +1 -1
- package/dist/cli-spec.d.ts +3 -0
- package/dist/cli-spec.d.ts.map +1 -0
- package/dist/cli-spec.js +667 -0
- package/dist/cli-spec.js.map +1 -0
- package/dist/cli.d.ts +146 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +80 -14
- package/dist/cli.js.map +1 -1
- package/dist/cmdspec.json +1219 -0
- package/dist/commands.d.ts +78 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +231 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/db.d.ts +6 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +7 -1
- package/dist/db.js.map +1 -1
- package/dist/discovery.d.ts +22 -3
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +91 -12
- package/dist/discovery.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +66 -1
- package/dist/doctor.js.map +1 -1
- package/dist/help.d.ts +10 -6
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +118 -371
- package/dist/help.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/init/plan.js +4 -3
- package/dist/init/plan.js.map +1 -1
- package/dist/init/templates.d.ts.map +1 -1
- package/dist/init/templates.js +4 -2
- package/dist/init/templates.js.map +1 -1
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +12 -6
- package/dist/preview.js.map +1 -1
- package/dist/search.d.ts +2 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +66 -22
- package/dist/search.js.map +1 -1
- package/dist/spec.d.ts +21 -1
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js +24 -2
- package/dist/spec.js.map +1 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +99 -1
- package/dist/sync.js.map +1 -1
- package/package.json +8 -5
- package/src/build.ts +86 -7
- package/src/cli-spec.ts +688 -0
- package/src/cli.ts +90 -17
- package/src/commands.ts +298 -0
- package/src/config.ts +4 -1
- package/src/db.ts +9 -2
- package/src/discovery.ts +113 -12
- package/src/doctor.ts +67 -0
- package/src/help.ts +138 -380
- package/src/index.ts +1 -0
- package/src/init/plan.ts +4 -3
- package/src/init/templates.ts +4 -2
- package/src/preview.ts +14 -13
- package/src/search.ts +79 -22
- package/src/spec.ts +28 -2
- package/src/sync.ts +120 -1
package/src/discovery.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
3
3
|
import { DocspackError } from "./errors.js";
|
|
4
4
|
import {
|
|
5
|
+
cliPackageId,
|
|
6
|
+
DISCOVERABLE_NAMES,
|
|
5
7
|
isCommunityPackage,
|
|
6
8
|
isDocsPackage,
|
|
7
9
|
LLMS_DIR,
|
|
@@ -38,17 +40,20 @@ export interface Discovery {
|
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
|
-
* Finds the documentation packages a project depends on: every `@vendor/docspack
|
|
42
|
-
* `@docspack-community/*` entry in `dependencies` or
|
|
43
|
-
* ships a `.llms/manifest.json`.
|
|
43
|
+
* Finds the documentation packages a project depends on: every `@vendor/docspack`,
|
|
44
|
+
* `@vendor/<name>-docspack` or `@docspack-community/*` entry in `dependencies` or
|
|
45
|
+
* `devDependencies` that is installed and ships a `.llms/manifest.json`.
|
|
46
|
+
*
|
|
47
|
+
* A dependency that ships a payload under any other name is reported in `problems` rather than
|
|
48
|
+
* indexed, so a pack nobody can read says so instead of going missing from every answer.
|
|
44
49
|
*/
|
|
45
50
|
export async function discoverPackages(cwd: string): Promise<Discovery> {
|
|
46
51
|
const root = resolve(cwd);
|
|
47
|
-
const declared = await
|
|
52
|
+
const declared = await declaredDependencies(root);
|
|
48
53
|
const packages: DiscoveredPackage[] = [];
|
|
49
54
|
const problems: string[] = [];
|
|
50
55
|
|
|
51
|
-
for (const name of declared) {
|
|
56
|
+
for (const name of declared.filter(isDocsPackage)) {
|
|
52
57
|
const dir = await resolvePackageDir(name, root);
|
|
53
58
|
if (dir === undefined) {
|
|
54
59
|
problems.push(`${name} is declared in package.json but is not installed`);
|
|
@@ -94,10 +99,62 @@ export async function discoverPackages(cwd: string): Promise<Discovery> {
|
|
|
94
99
|
});
|
|
95
100
|
}
|
|
96
101
|
|
|
102
|
+
problems.push(...(await skippedPayloads(declared.filter(isNotDocsPackage), root)));
|
|
103
|
+
|
|
97
104
|
packages.sort((a, b) => a.name.localeCompare(b.name));
|
|
98
105
|
return { packages, problems };
|
|
99
106
|
}
|
|
100
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Dependencies that ship a payload under a name discovery does not match.
|
|
110
|
+
*
|
|
111
|
+
* None of them is indexed, and that is the point: the name check is what decides whether a
|
|
112
|
+
* package may put content in front of a model, and widening it to "carries a manifest" would let
|
|
113
|
+
* any transitive dependency opt itself in. But a package built by `docspack build` and then named
|
|
114
|
+
* outside the discoverable shapes is a publishing mistake — a typo, a rename, or a reading of the
|
|
115
|
+
* naming rule — and saying nothing about it is worse than refusing it. The corpus is absent from
|
|
116
|
+
* every answer while the question it should have answered is answered confidently by another pack.
|
|
117
|
+
*/
|
|
118
|
+
async function skippedPayloads(names: readonly string[], root: string): Promise<string[]> {
|
|
119
|
+
// Concurrent, unlike the loop over docs packages above: this one runs over every dependency
|
|
120
|
+
// the project declares rather than the handful that document something, and `docspack ask`
|
|
121
|
+
// pays for it on each question. The reads are independent, and `Promise.all` keeps the order
|
|
122
|
+
// of `names` so the report is the same on every run.
|
|
123
|
+
const found = await Promise.all(names.map((name) => skippedPayload(name, root)));
|
|
124
|
+
return found.filter((problem) => problem !== undefined);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function skippedPayload(name: string, root: string): Promise<string | undefined> {
|
|
128
|
+
const dir = await resolvePackageDir(name, root);
|
|
129
|
+
if (dir === undefined) return undefined;
|
|
130
|
+
|
|
131
|
+
let raw: string;
|
|
132
|
+
try {
|
|
133
|
+
raw = await readFile(join(dir, LLMS_DIR, MANIFEST_FILE), "utf8");
|
|
134
|
+
} catch {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Counted rather than validated: a manifest too malformed to parse is still a payload its
|
|
139
|
+
// publisher meant to be read, and the name is the problem to report either way.
|
|
140
|
+
let chunks = 0;
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(raw) as { chunks?: unknown };
|
|
143
|
+
if (Array.isArray(parsed.chunks)) chunks = parsed.chunks.length;
|
|
144
|
+
} catch {
|
|
145
|
+
// Not JSON. Report the name anyway; `docspack doctor` is where the payload is checked.
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
`${name} ships ${LLMS_DIR}/${MANIFEST_FILE} with ${chunks} ${chunks === 1 ? "chunk" : "chunks"} but was not indexed: ` +
|
|
150
|
+
`its name is not a discoverable pack shape (${DISCOVERABLE_NAMES})`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isNotDocsPackage(name: string): boolean {
|
|
155
|
+
return !isDocsPackage(name);
|
|
156
|
+
}
|
|
157
|
+
|
|
101
158
|
/**
|
|
102
159
|
* Every library this project depends on directly, installed and readable.
|
|
103
160
|
*
|
|
@@ -106,7 +163,7 @@ export async function discoverPackages(cwd: string): Promise<Discovery> {
|
|
|
106
163
|
*/
|
|
107
164
|
export async function discoverLibraries(cwd: string): Promise<readonly DiscoveredLibrary[]> {
|
|
108
165
|
const root = resolve(cwd);
|
|
109
|
-
const names = (await declaredDependencies(root)).filter(
|
|
166
|
+
const names = (await declaredDependencies(root)).filter(isNotDocsPackage);
|
|
110
167
|
const libraries: DiscoveredLibrary[] = [];
|
|
111
168
|
|
|
112
169
|
for (const name of names) {
|
|
@@ -122,17 +179,61 @@ export async function discoverLibraries(cwd: string): Promise<readonly Discovere
|
|
|
122
179
|
return libraries;
|
|
123
180
|
}
|
|
124
181
|
|
|
182
|
+
/** An installed library that describes its own command-line interface. */
|
|
183
|
+
export interface DiscoveredCommandLine extends DiscoveredLibrary {
|
|
184
|
+
/** The id its commands are indexed under: the library's, marked as a CLI. */
|
|
185
|
+
readonly id: string;
|
|
186
|
+
/** Each description it names, as absolute paths inside the package. */
|
|
187
|
+
readonly documents: readonly string[];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Libraries whose `package.json` names a cmdspec document (`packages/cmdspec/SPEC.md` §13):
|
|
192
|
+
* `"cmdspec": "./cmdspec.json"`, or one per executable, `"cmdspec": { "tool": "./tool.json" }`.
|
|
193
|
+
*
|
|
194
|
+
* A path that resolves outside the package is ignored. The field is a dependency's own metadata,
|
|
195
|
+
* and reading wherever it points would let any installed package put any file on this machine
|
|
196
|
+
* into the index.
|
|
197
|
+
*/
|
|
198
|
+
export async function discoverCommandLines(
|
|
199
|
+
cwd: string,
|
|
200
|
+
libraries?: readonly DiscoveredLibrary[],
|
|
201
|
+
): Promise<DiscoveredCommandLine[]> {
|
|
202
|
+
const found: DiscoveredCommandLine[] = [];
|
|
203
|
+
for (const library of libraries ?? (await discoverLibraries(cwd))) {
|
|
204
|
+
let field: unknown;
|
|
205
|
+
try {
|
|
206
|
+
field = (
|
|
207
|
+
JSON.parse(await readFile(join(library.dir, "package.json"), "utf8")) as {
|
|
208
|
+
cmdspec?: unknown;
|
|
209
|
+
}
|
|
210
|
+
).cmdspec;
|
|
211
|
+
} catch {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const named =
|
|
215
|
+
typeof field === "string"
|
|
216
|
+
? [field]
|
|
217
|
+
: typeof field === "object" && field !== null && !Array.isArray(field)
|
|
218
|
+
? Object.values(field).filter((value): value is string => typeof value === "string")
|
|
219
|
+
: [];
|
|
220
|
+
const root = resolve(library.dir);
|
|
221
|
+
const documents = named
|
|
222
|
+
.map((path) => resolve(root, path))
|
|
223
|
+
.filter((path) => path.startsWith(`${root}${sep}`));
|
|
224
|
+
if (documents.length > 0) {
|
|
225
|
+
found.push({ ...library, id: cliPackageId(library.name, library.version), documents });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return found;
|
|
229
|
+
}
|
|
230
|
+
|
|
125
231
|
/** Package ids installed in this project, used to scope queries to the versions actually in use. */
|
|
126
232
|
export async function projectPackageIds(cwd: string): Promise<string[]> {
|
|
127
233
|
const { packages } = await discoverPackages(cwd);
|
|
128
234
|
return packages.map((pkg) => pkg.id);
|
|
129
235
|
}
|
|
130
236
|
|
|
131
|
-
/** Docs packages this project depends on, the input to `discoverPackages`. */
|
|
132
|
-
async function declaredDocsPackages(cwd: string): Promise<string[]> {
|
|
133
|
-
return (await declaredDependencies(cwd)).filter(isDocsPackage);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
237
|
/**
|
|
137
238
|
* Every dependency declared by this directory or by an ancestor of it. A workspace declares
|
|
138
239
|
* shared tooling in the repository root and its members inherit the install, so reading only
|
package/src/doctor.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { Dirent } from "node:fs";
|
|
2
2
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { commandFindings } from "./commands.js";
|
|
4
5
|
import { readBuildConfig } from "./config.js";
|
|
5
6
|
import { measureCoverage } from "./coverage.js";
|
|
6
7
|
import { PLACEHOLDER } from "./init/templates.js";
|
|
7
8
|
import {
|
|
9
|
+
DISCOVERABLE_NAMES,
|
|
8
10
|
estimateTokens,
|
|
11
|
+
isDocsPackage,
|
|
9
12
|
LLMS_DIR,
|
|
10
13
|
MANIFEST_FILE,
|
|
11
14
|
type PackageManifest,
|
|
@@ -208,6 +211,7 @@ export async function runDoctor(options: DoctorOptions): Promise<DoctorReport> {
|
|
|
208
211
|
}
|
|
209
212
|
|
|
210
213
|
findings.push(...(await coverageFindings(options.dir, llmsDir, manifest)));
|
|
214
|
+
findings.push(...(await commandLineFindings(options.dir)));
|
|
211
215
|
|
|
212
216
|
if (manifest.chunks.every((chunk) => chunk.entities.length === 0)) {
|
|
213
217
|
findings.push({
|
|
@@ -347,6 +351,21 @@ function checkPackageJson(
|
|
|
347
351
|
});
|
|
348
352
|
}
|
|
349
353
|
|
|
354
|
+
// The one property that decides whether the indexer will open the package at all, and the one
|
|
355
|
+
// this check used to skip: every other check here describes a package that indexes badly, and
|
|
356
|
+
// a name outside the discoverable shapes describes one that is never read. It publishes, it
|
|
357
|
+
// installs, it passes every other check, and the corpus is absent from every answer.
|
|
358
|
+
const name = typeof pkg.name === "string" ? pkg.name : manifest.name;
|
|
359
|
+
if (!isDocsPackage(name)) {
|
|
360
|
+
findings.push({
|
|
361
|
+
check: "name-undiscoverable",
|
|
362
|
+
severity: "error",
|
|
363
|
+
message: `"${name}" is not a name the indexer discovers; the package would install and never be read`,
|
|
364
|
+
where: "package.json",
|
|
365
|
+
fix: `Rename it to one of ${DISCOVERABLE_NAMES}.`,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
350
369
|
const files = Array.isArray(pkg.files) ? pkg.files.map(String) : undefined;
|
|
351
370
|
if (
|
|
352
371
|
files !== undefined &&
|
|
@@ -373,6 +392,7 @@ async function newestSourceMtime(dir: string): Promise<number> {
|
|
|
373
392
|
const config = await readBuildConfig(dir);
|
|
374
393
|
const newest = await Promise.all([
|
|
375
394
|
config.openapi === undefined ? 0 : mtime(join(dir, config.openapi)),
|
|
395
|
+
config.cmdspec === undefined ? 0 : mtime(join(dir, config.cmdspec)),
|
|
376
396
|
config.from === undefined ? 0 : newestInTree(join(dir, config.from)),
|
|
377
397
|
]);
|
|
378
398
|
return Math.max(...newest);
|
|
@@ -445,6 +465,53 @@ async function coverageFindings(
|
|
|
445
465
|
return findings;
|
|
446
466
|
}
|
|
447
467
|
|
|
468
|
+
/**
|
|
469
|
+
* The configured CLI description, checked the way `build` checks it: invalid is an error, because
|
|
470
|
+
* the next build will refuse it; the rest is `commandFindings`. Nothing to check without
|
|
471
|
+
* `docspack.cmdspec`, since a package built from a flag does not say where its description is.
|
|
472
|
+
*/
|
|
473
|
+
async function commandLineFindings(dir: string): Promise<Finding[]> {
|
|
474
|
+
const source = (await readBuildConfig(dir)).cmdspec;
|
|
475
|
+
if (source === undefined) return [];
|
|
476
|
+
const file = join(dir, source);
|
|
477
|
+
const { convertText, readSource } = await import("@docspack/cmdspec/read");
|
|
478
|
+
const { validate } = await import("@docspack/cmdspec/validate");
|
|
479
|
+
let document: Awaited<ReturnType<typeof convertText>>["document"];
|
|
480
|
+
try {
|
|
481
|
+
({ document } = await convertText(await readSource(file), { where: source }));
|
|
482
|
+
} catch (error) {
|
|
483
|
+
return [
|
|
484
|
+
{
|
|
485
|
+
check: "cli-unreadable",
|
|
486
|
+
severity: "error",
|
|
487
|
+
message: `${source}: ${error instanceof Error ? error.message : String(error)}`,
|
|
488
|
+
fix: "Point docspack.cmdspec at a cmdspec, OpenCLI or Usage document.",
|
|
489
|
+
},
|
|
490
|
+
];
|
|
491
|
+
}
|
|
492
|
+
const result = validate(document);
|
|
493
|
+
if (!result.valid) {
|
|
494
|
+
return [
|
|
495
|
+
{
|
|
496
|
+
check: "cli-invalid",
|
|
497
|
+
severity: "error",
|
|
498
|
+
message: `${source} is not a valid CLI description: ${result.problems
|
|
499
|
+
.slice(0, 3)
|
|
500
|
+
.map((problem) => `${problem.at}: ${problem.message}`)
|
|
501
|
+
.join("; ")}${result.problems.length > 3 ? `; …${result.problems.length - 3} more` : ""}`,
|
|
502
|
+
where: source,
|
|
503
|
+
fix: `Run \`npx @docspack/cmdspec validate ${source}\` for every problem.`,
|
|
504
|
+
},
|
|
505
|
+
];
|
|
506
|
+
}
|
|
507
|
+
return commandFindings(document).map((finding) => ({
|
|
508
|
+
check: "cli-incomplete",
|
|
509
|
+
severity: finding.severity,
|
|
510
|
+
message: finding.message,
|
|
511
|
+
where: source,
|
|
512
|
+
}));
|
|
513
|
+
}
|
|
514
|
+
|
|
448
515
|
async function readJson(path: string): Promise<Record<string, unknown> | undefined> {
|
|
449
516
|
try {
|
|
450
517
|
const parsed: unknown = JSON.parse(await readFile(path, "utf8"));
|