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/preview.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { commandIndex, matchCommands } from "./commands.js";
|
|
3
4
|
import { type IndexedChunk, type SearchHit, Store } from "./db.js";
|
|
4
5
|
import { endpointIndex, matchEndpoints } from "./endpoints.js";
|
|
5
6
|
import { DocspackError } from "./errors.js";
|
|
@@ -86,32 +87,32 @@ export async function previewPackage(options: PreviewOptions): Promise<PreviewRe
|
|
|
86
87
|
...(documents.length === 0 ? {} : { documents }),
|
|
87
88
|
});
|
|
88
89
|
|
|
89
|
-
// The same endpoint pinning `queryDocs` does, because `preview` exists to show an author what an
|
|
90
|
+
// The same endpoint and command pinning `queryDocs` does, because `preview` exists to show an author what an
|
|
90
91
|
// agent would receive. A preview that ranked where the real query path pins would send them
|
|
91
92
|
// chasing a retrieval problem they do not have.
|
|
92
93
|
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
93
94
|
const maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
)
|
|
103
|
-
.map((
|
|
95
|
+
const addressable = manifest.chunks.map((chunk) => ({
|
|
96
|
+
chunkId: chunkId(id, chunk.id),
|
|
97
|
+
entities: chunk.entities,
|
|
98
|
+
}));
|
|
99
|
+
const pinnedIds = [
|
|
100
|
+
...matchEndpoints(options.query, endpointIndex(addressable)),
|
|
101
|
+
...matchCommands(options.query, commandIndex(addressable)),
|
|
102
|
+
].map((match) => match.chunkId);
|
|
103
|
+
const pinned = [...new Set(pinnedIds)]
|
|
104
|
+
.map((pinnedId) => store.chunk(pinnedId))
|
|
104
105
|
.filter((chunk): chunk is SearchHit => chunk !== undefined)
|
|
105
106
|
.map(describe);
|
|
106
107
|
const spent = pinned.reduce((total, hit) => total + hit.tokens, 0);
|
|
107
|
-
const
|
|
108
|
+
const pinnedSet = new Set(pinned.map((hit) => hit.chunkId));
|
|
108
109
|
|
|
109
110
|
const ranked = store
|
|
110
111
|
.search(options.query, {
|
|
111
112
|
limit: Math.max(1, limit - pinned.length),
|
|
112
113
|
maxTokens: Math.max(1, maxTokens - spent),
|
|
113
114
|
})
|
|
114
|
-
.filter((hit) => !
|
|
115
|
+
.filter((hit) => !pinnedSet.has(hit.chunkId))
|
|
115
116
|
.map(describe);
|
|
116
117
|
|
|
117
118
|
const hits = [...pinned, ...ranked];
|
package/src/search.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { COMMAND_ENTITY, type CommandEntry, commandIndex, matchCommands } from "./commands.js";
|
|
1
2
|
import type { PackageKind, SearchHit, Store } from "./db.js";
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type DiscoveredLibrary,
|
|
5
|
+
discoverCommandLines,
|
|
6
|
+
discoverLibraries,
|
|
7
|
+
discoverPackages,
|
|
8
|
+
} from "./discovery.js";
|
|
3
9
|
import { endpointIndex, matchEndpoints } from "./endpoints.js";
|
|
4
|
-
import { chunkId, formatDocumentedLibrary, isCommunityPackage } from "./spec.js";
|
|
10
|
+
import { CLI_SUFFIX, chunkId, formatDocumentedLibrary, isCommunityPackage } from "./spec.js";
|
|
5
11
|
|
|
6
12
|
/** Ceiling on how much context one query may return, per the blueprint's context-exhaustion rule. */
|
|
7
13
|
export const DEFAULT_MAX_TOKENS = 3000;
|
|
@@ -76,15 +82,27 @@ export interface QueryResult {
|
|
|
76
82
|
* chunk, and has to be told that the match was against a template.
|
|
77
83
|
*/
|
|
78
84
|
readonly endpoints: readonly string[];
|
|
85
|
+
/** Command paths the question named that a described command answers, pinned likewise. */
|
|
86
|
+
readonly commands: readonly string[];
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
export async function queryDocs(options: QueryOptions): Promise<QueryResult> {
|
|
82
90
|
const scoped = options.scoped !== false;
|
|
83
91
|
const installed = scoped ? (await discoverPackages(options.cwd)).packages : undefined;
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
.map((
|
|
92
|
+
// Read once: the libraries scope both the declarations and the CLIs a question can reach.
|
|
93
|
+
const libraries = scoped ? await discoverLibraries(options.cwd) : undefined;
|
|
94
|
+
const commandLines = scoped
|
|
95
|
+
? (await discoverCommandLines(options.cwd, libraries)).map((cli) => cli.id)
|
|
96
|
+
: options.store
|
|
97
|
+
.listPackages()
|
|
98
|
+
.filter((pkg) => pkg.kind === "cli")
|
|
99
|
+
.map((pkg) => pkg.id);
|
|
100
|
+
const packageIds =
|
|
101
|
+
installed === undefined ? undefined : [...installed.map((pkg) => pkg.id), ...commandLines];
|
|
102
|
+
const unindexed = [
|
|
103
|
+
...(installed ?? []).map((pkg) => pkg.id),
|
|
104
|
+
...(scoped ? commandLines : []),
|
|
105
|
+
].filter((id) => !options.store.hasPackage(id));
|
|
88
106
|
|
|
89
107
|
// Read from the installed manifests rather than the index: the store is a cache of chunk text,
|
|
90
108
|
// and adding a column to it would make every existing store need a rebuild to answer this.
|
|
@@ -107,9 +125,25 @@ export async function queryDocs(options: QueryOptions): Promise<QueryResult> {
|
|
|
107
125
|
const maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
108
126
|
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
109
127
|
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
const
|
|
128
|
+
// A described CLI's command paths: from a docs package's manifest entities, and from the name
|
|
129
|
+
// table of each CLI read out of an installed package by `sync`.
|
|
130
|
+
const commandEntries = [
|
|
131
|
+
...commandIndex(operations),
|
|
132
|
+
...commandLines.flatMap((id) =>
|
|
133
|
+
options.store
|
|
134
|
+
.symbolTable(id)
|
|
135
|
+
.filter((row) => row.name.startsWith(COMMAND_ENTITY))
|
|
136
|
+
.map((row) => ({ chunkId: row.chunkId, path: row.name.slice(COMMAND_ENTITY.length) })),
|
|
137
|
+
),
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
// Addressed chunks first, and the ranker is then asked for less: a pinned operation or command
|
|
141
|
+
// is the answer, and three ranked chunks beside it would spend the budget restating it.
|
|
142
|
+
const {
|
|
143
|
+
hits: pinned,
|
|
144
|
+
endpoints,
|
|
145
|
+
commands,
|
|
146
|
+
} = findAddressed(options, operations, commandEntries, documented, maxTokens);
|
|
113
147
|
const spent = pinned.reduce((total, hit) => total + hit.tokens, 0);
|
|
114
148
|
const pinnedIds = new Set(pinned.map((hit) => hit.chunkId));
|
|
115
149
|
|
|
@@ -123,8 +157,9 @@ export async function queryDocs(options: QueryOptions): Promise<QueryResult> {
|
|
|
123
157
|
maxTokens: Math.max(1, maxTokens - spent),
|
|
124
158
|
// Declarations are addressed by name, never ranked against prose: a library exports far
|
|
125
159
|
// more names than its documentation has pages, and ranking them together would answer
|
|
126
|
-
// every question with type machinery.
|
|
127
|
-
|
|
160
|
+
// every question with type machinery. A CLI's command chunks are prose about what to type,
|
|
161
|
+
// one per command, and are ranked like the documentation they are.
|
|
162
|
+
kinds: ["docs", "cli"],
|
|
128
163
|
})
|
|
129
164
|
.filter((hit) => !pinnedIds.has(hit.chunkId))
|
|
130
165
|
.map((hit) => toQueryHit(hit, "docs", documented));
|
|
@@ -132,7 +167,7 @@ export async function queryDocs(options: QueryOptions): Promise<QueryResult> {
|
|
|
132
167
|
const { declarations, undocumented } =
|
|
133
168
|
options.artifacts === false
|
|
134
169
|
? { declarations: [] as QueryHit[], undocumented: [] as string[] }
|
|
135
|
-
: await findDeclarations(options, [...pinned, ...prose], maxTokens - spent);
|
|
170
|
+
: await findDeclarations(options, [...pinned, ...prose], maxTokens - spent, libraries);
|
|
136
171
|
|
|
137
172
|
// Pinned operations, then declarations, then the ranked prose. Both of the first two were
|
|
138
173
|
// addressed by name rather than guessed at, and for an agent about to write a call the exact
|
|
@@ -146,39 +181,55 @@ export async function queryDocs(options: QueryOptions): Promise<QueryResult> {
|
|
|
146
181
|
unindexed,
|
|
147
182
|
undocumented,
|
|
148
183
|
endpoints,
|
|
184
|
+
commands,
|
|
149
185
|
};
|
|
150
186
|
}
|
|
151
187
|
|
|
152
188
|
/**
|
|
153
|
-
* Operation chunks the question addressed.
|
|
189
|
+
* Operation and command chunks the question addressed — operations first, then commands.
|
|
154
190
|
*
|
|
155
191
|
* Bounded by the same token budget as everything else, and the first match is always kept: an answer
|
|
156
|
-
* whose one pinned
|
|
192
|
+
* whose one pinned chunk did not fit would have pinned nothing and said nothing about it.
|
|
157
193
|
*/
|
|
158
|
-
function
|
|
194
|
+
function findAddressed(
|
|
159
195
|
options: QueryOptions,
|
|
160
196
|
operations: readonly { chunkId: string; entities: readonly string[] }[],
|
|
197
|
+
commandEntries: readonly CommandEntry[],
|
|
161
198
|
documented: ReadonlyMap<string, readonly string[]>,
|
|
162
199
|
maxTokens: number,
|
|
163
|
-
): { hits: QueryHit[]; endpoints: string[] } {
|
|
164
|
-
const matches =
|
|
200
|
+
): { hits: QueryHit[]; endpoints: string[]; commands: string[] } {
|
|
201
|
+
const matches = [
|
|
202
|
+
...matchEndpoints(options.query, endpointIndex(operations)).map((match) => ({
|
|
203
|
+
chunkId: match.chunkId,
|
|
204
|
+
endpoint: `${match.method} ${match.path}`,
|
|
205
|
+
})),
|
|
206
|
+
...matchCommands(options.query, commandEntries).map((match) => ({
|
|
207
|
+
chunkId: match.chunkId,
|
|
208
|
+
command: match.path,
|
|
209
|
+
})),
|
|
210
|
+
];
|
|
165
211
|
const hits: QueryHit[] = [];
|
|
166
212
|
const endpoints: string[] = [];
|
|
213
|
+
const commands: string[] = [];
|
|
214
|
+
const seen = new Set<string>();
|
|
167
215
|
let spent = 0;
|
|
168
216
|
|
|
169
217
|
for (const match of matches) {
|
|
218
|
+
if (seen.has(match.chunkId)) continue;
|
|
170
219
|
if (options.packageFilter !== undefined && !match.chunkId.includes(options.packageFilter)) {
|
|
171
220
|
continue;
|
|
172
221
|
}
|
|
173
222
|
const chunk = options.store.chunk(match.chunkId);
|
|
174
223
|
if (chunk === undefined) continue;
|
|
175
224
|
if (hits.length > 0 && spent + chunk.tokens > maxTokens) break;
|
|
225
|
+
seen.add(match.chunkId);
|
|
176
226
|
hits.push(toQueryHit(chunk, "docs", documented));
|
|
177
|
-
endpoints.push(
|
|
227
|
+
if ("endpoint" in match) endpoints.push(match.endpoint);
|
|
228
|
+
else commands.push(match.command);
|
|
178
229
|
spent += chunk.tokens;
|
|
179
230
|
}
|
|
180
231
|
|
|
181
|
-
return { hits, endpoints };
|
|
232
|
+
return { hits, endpoints, commands };
|
|
182
233
|
}
|
|
183
234
|
|
|
184
235
|
/**
|
|
@@ -192,11 +243,12 @@ async function findDeclarations(
|
|
|
192
243
|
options: QueryOptions,
|
|
193
244
|
prose: readonly QueryHit[],
|
|
194
245
|
maxTokens: number,
|
|
246
|
+
libraries: readonly DiscoveredLibrary[] | undefined,
|
|
195
247
|
): Promise<{ declarations: QueryHit[]; undocumented: string[] }> {
|
|
196
248
|
const scope =
|
|
197
249
|
options.scoped === false
|
|
198
250
|
? undefined
|
|
199
|
-
: (await discoverLibraries(options.cwd)).map(
|
|
251
|
+
: (libraries ?? (await discoverLibraries(options.cwd))).map(
|
|
200
252
|
(library) => `${library.name}@${library.version}`,
|
|
201
253
|
);
|
|
202
254
|
|
|
@@ -261,7 +313,8 @@ function toQueryHit(
|
|
|
261
313
|
...hit,
|
|
262
314
|
name,
|
|
263
315
|
version,
|
|
264
|
-
kind
|
|
316
|
+
// A CLI's chunks are found by ranking and by address alike; the id says which kind they are.
|
|
317
|
+
kind: hit.packageId.endsWith(CLI_SUFFIX) ? "cli" : kind,
|
|
265
318
|
trusted: !isCommunityPackage(name),
|
|
266
319
|
...(documents === undefined ? {} : { documents }),
|
|
267
320
|
};
|
|
@@ -297,7 +350,9 @@ export function renderAnswer(result: QueryResult, query: string): string {
|
|
|
297
350
|
const source =
|
|
298
351
|
hit.kind === "artifact"
|
|
299
352
|
? `Source: ${hit.name}@${hit.version} — declared in ${hit.filePath}, read from the installed package`
|
|
300
|
-
:
|
|
353
|
+
: hit.kind === "cli"
|
|
354
|
+
? `Source: ${hit.name}@${hit.version} — ${hit.filePath}, the installed package's own description of its command line`
|
|
355
|
+
: `Source: ${hit.name}@${hit.version} — ${hit.filePath}${describes}`;
|
|
301
356
|
return [`## ${hit.chunkId}${trust}`, source, "", hit.content].join("\n");
|
|
302
357
|
});
|
|
303
358
|
|
|
@@ -322,6 +377,8 @@ function undocumentedNotice(result: QueryResult): string {
|
|
|
322
377
|
|
|
323
378
|
/** Splits `@stripe/docspack@2025.4.1` into its name and version. */
|
|
324
379
|
export function splitPackageId(id: string): { name: string; version: string } {
|
|
380
|
+
// A CLI's commands are indexed under the library's id with a marker; the release is the same.
|
|
381
|
+
if (id.endsWith(CLI_SUFFIX)) return splitPackageId(id.slice(0, -CLI_SUFFIX.length));
|
|
325
382
|
const at = id.lastIndexOf("@");
|
|
326
383
|
if (at <= 0) return { name: id, version: "" };
|
|
327
384
|
return { name: id.slice(0, at), version: id.slice(at + 1) };
|
package/src/spec.ts
CHANGED
|
@@ -46,9 +46,21 @@ export interface PackageManifest {
|
|
|
46
46
|
|
|
47
47
|
const CHUNK_ID = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
48
48
|
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Official vendor packages: `@stripe/docspack`, and its siblings `@stripe/openapi-docspack`.
|
|
51
|
+
*
|
|
52
|
+
* The suffixed form is for a vendor that also redistributes somebody else's documentation, where
|
|
53
|
+
* licence, version, cadence and attribution belong to a different upstream and cannot be merged
|
|
54
|
+
* into one package. It costs nothing the single name was protecting: discovery stays a pure name
|
|
55
|
+
* check against `package.json`, and npm scope ownership is the boundary trust rests on — whoever
|
|
56
|
+
* can publish `@stripe/docspack` can publish `@stripe/openapi-docspack` and nobody else can.
|
|
57
|
+
*
|
|
58
|
+
* Derivation is a separate question and unchanged: `@stripe/docspack` is the one name inferable
|
|
59
|
+
* from a dependency on `@stripe/sdk`, so that is still the only name `init` suggests. Derive to
|
|
60
|
+
* suggest; match to discover.
|
|
61
|
+
*/
|
|
50
62
|
export function isVendorPackage(name: string): boolean {
|
|
51
|
-
return /^@[^/]+\/docspack$/.test(name);
|
|
63
|
+
return /^@[^/]+\/(?:[^/]+-)?docspack$/.test(name);
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
/** Community packages: `@docspack-community/jira`. */
|
|
@@ -60,11 +72,25 @@ export function isDocsPackage(name: string): boolean {
|
|
|
60
72
|
return isVendorPackage(name) || isCommunityPackage(name);
|
|
61
73
|
}
|
|
62
74
|
|
|
75
|
+
/** The discoverable name shapes, for an error that has to say what it would have accepted. */
|
|
76
|
+
export const DISCOVERABLE_NAMES =
|
|
77
|
+
"@vendor/docspack, @vendor/<name>-docspack or @docspack-community/<name>";
|
|
78
|
+
|
|
63
79
|
/** Stable identifier used as the primary key in the store: `@stripe/docspack@2025.4.1`. */
|
|
64
80
|
export function packageId(name: string, version: string): string {
|
|
65
81
|
return `${name}@${version}`;
|
|
66
82
|
}
|
|
67
83
|
|
|
84
|
+
/**
|
|
85
|
+
* What marks a package of command chunks derived from an installed CLI's own description. Its id
|
|
86
|
+
* cannot be the library's, which its type declarations already hold.
|
|
87
|
+
*/
|
|
88
|
+
export const CLI_SUFFIX = "#cli";
|
|
89
|
+
|
|
90
|
+
export function cliPackageId(name: string, version: string): string {
|
|
91
|
+
return `${packageId(name, version)}${CLI_SUFFIX}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
68
94
|
export function chunkId(pkgId: string, chunk: string): string {
|
|
69
95
|
return `${pkgId}/${chunk}`;
|
|
70
96
|
}
|
package/src/sync.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { relative } from "node:path";
|
|
2
3
|
import { readArtifact } from "./artifact.js";
|
|
4
|
+
import { COMMAND_ENTITY, commandChunks } from "./commands.js";
|
|
3
5
|
import type { IndexedChunk, PackageKind, Store } from "./db.js";
|
|
4
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type DiscoveredCommandLine,
|
|
8
|
+
type DiscoveredPackage,
|
|
9
|
+
discoverCommandLines,
|
|
10
|
+
discoverLibraries,
|
|
11
|
+
discoverPackages,
|
|
12
|
+
} from "./discovery.js";
|
|
5
13
|
import { chunkId, estimateTokens, resolveChunkFile } from "./spec.js";
|
|
6
14
|
|
|
7
15
|
export interface SyncOptions {
|
|
@@ -84,10 +92,121 @@ export async function syncProject(options: SyncOptions): Promise<SyncResult> {
|
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
if (options.artifacts !== false) synced.push(...(await syncArtifacts(options)));
|
|
95
|
+
synced.push(...(await syncCommandLines(options, problems)));
|
|
87
96
|
|
|
88
97
|
return { packages: synced, problems };
|
|
89
98
|
}
|
|
90
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Indexes the command-line interface each installed library describes in its own `package.json`
|
|
102
|
+
* (`"cmdspec"`, `packages/cmdspec/SPEC.md` §13): one chunk per command, ranked like documentation
|
|
103
|
+
* and addressed by command path, for exactly the version installed.
|
|
104
|
+
*
|
|
105
|
+
* Not tied to `--no-artifacts`. That flag skips the declarations, which are many and machine-made;
|
|
106
|
+
* a CLI's description is one document its authors wrote, and leaving it out would make `ask` report
|
|
107
|
+
* the package as installed but not indexed on every question.
|
|
108
|
+
*/
|
|
109
|
+
async function syncCommandLines(
|
|
110
|
+
options: SyncOptions,
|
|
111
|
+
problems: string[],
|
|
112
|
+
): Promise<SyncedPackage[]> {
|
|
113
|
+
const commandLines = await discoverCommandLines(options.cwd);
|
|
114
|
+
if (commandLines.length === 0) return [];
|
|
115
|
+
const synced: SyncedPackage[] = [];
|
|
116
|
+
|
|
117
|
+
for (const cli of commandLines) {
|
|
118
|
+
if (options.force !== true && options.store.hasPackage(cli.id)) {
|
|
119
|
+
synced.push({
|
|
120
|
+
id: cli.id,
|
|
121
|
+
name: cli.name,
|
|
122
|
+
version: cli.version,
|
|
123
|
+
chunks: options.store.countChunks(cli.id),
|
|
124
|
+
tokens: 0,
|
|
125
|
+
status: "cached",
|
|
126
|
+
trusted: true,
|
|
127
|
+
kind: "cli",
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
options.onProgress?.(`reading ${cli.id}`);
|
|
133
|
+
const { chunks, symbols } = await readCommandLine(cli, problems);
|
|
134
|
+
if (chunks.length === 0) continue;
|
|
135
|
+
options.store.indexPackage(
|
|
136
|
+
{ id: cli.id, name: cli.name, version: cli.version, kind: "cli" },
|
|
137
|
+
chunks,
|
|
138
|
+
symbols,
|
|
139
|
+
);
|
|
140
|
+
synced.push({
|
|
141
|
+
id: cli.id,
|
|
142
|
+
name: cli.name,
|
|
143
|
+
version: cli.version,
|
|
144
|
+
chunks: chunks.length,
|
|
145
|
+
tokens: chunks.reduce((total, chunk) => total + chunk.tokens, 0),
|
|
146
|
+
status: "indexed",
|
|
147
|
+
trusted: true,
|
|
148
|
+
kind: "cli",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return synced;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A library's CLI descriptions as chunks, and its command paths as names pointing at them.
|
|
156
|
+
*
|
|
157
|
+
* A description that is invalid, or that describes another version than the one installed, is
|
|
158
|
+
* reported and skipped: indexing it would answer with a command line the installed program does
|
|
159
|
+
* not accept, which is the one failure this tool exists to prevent.
|
|
160
|
+
*/
|
|
161
|
+
async function readCommandLine(
|
|
162
|
+
cli: DiscoveredCommandLine,
|
|
163
|
+
problems: string[],
|
|
164
|
+
): Promise<{ chunks: IndexedChunk[]; symbols: Map<string, string> }> {
|
|
165
|
+
const { readFile: readDocument } = await import("@docspack/cmdspec/read");
|
|
166
|
+
const chunks: IndexedChunk[] = [];
|
|
167
|
+
const symbols = new Map<string, string>();
|
|
168
|
+
const taken = new Set<string>();
|
|
169
|
+
|
|
170
|
+
for (const path of cli.documents) {
|
|
171
|
+
const where = relative(cli.dir, path);
|
|
172
|
+
let document: Awaited<ReturnType<typeof readDocument>>["document"];
|
|
173
|
+
try {
|
|
174
|
+
({ document } = await readDocument(path));
|
|
175
|
+
} catch (error) {
|
|
176
|
+
problems.push(
|
|
177
|
+
`${cli.name}: ${where} ${error instanceof Error ? error.message : String(error)}`,
|
|
178
|
+
);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (document.info.version !== cli.version) {
|
|
182
|
+
problems.push(
|
|
183
|
+
`${cli.name}: ${where} describes version ${document.info.version}, but ${cli.version} is installed — not indexed`,
|
|
184
|
+
);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const chunk of commandChunks(document)) {
|
|
189
|
+
// Two executables in one package each have an overview called `cli`.
|
|
190
|
+
let id = chunk.id;
|
|
191
|
+
for (let suffix = 2; taken.has(id); suffix += 1) id = `${chunk.id}-${suffix}`;
|
|
192
|
+
taken.add(id);
|
|
193
|
+
const content = `# ${chunk.title}\n\n${chunk.text}`;
|
|
194
|
+
const full = chunkId(cli.id, id);
|
|
195
|
+
chunks.push({
|
|
196
|
+
chunkId: full,
|
|
197
|
+
filePath: chunk.path === undefined ? where : `${where}#${chunk.path}`,
|
|
198
|
+
tokens: estimateTokens(content),
|
|
199
|
+
content,
|
|
200
|
+
tags: [...chunk.tags, ...chunk.entities],
|
|
201
|
+
});
|
|
202
|
+
for (const entity of chunk.entities) {
|
|
203
|
+
if (entity.startsWith(COMMAND_ENTITY)) symbols.set(entity, full);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { chunks, symbols };
|
|
208
|
+
}
|
|
209
|
+
|
|
91
210
|
/**
|
|
92
211
|
* Indexes each installed library's exported declarations.
|
|
93
212
|
*
|