@facetlayer/docs-tool 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/README.md +80 -0
- package/dist/__tests__/cli.test.d.ts +2 -0
- package/dist/__tests__/cli.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/browseLocalLibrary.d.ts +12 -0
- package/dist/browseLocalLibrary.d.ts.map +1 -0
- package/dist/browseNpmLibrary.d.ts +39 -0
- package/dist/browseNpmLibrary.d.ts.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +549 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +471 -0
- package/docs/project-setup.md +188 -0
- package/docs/writing-doc-files.md +156 -0
- package/package.json +44 -0
- package/src/__tests__/cli.test.ts +80 -0
- package/src/__tests__/index.test.ts +333 -0
- package/src/browseLocalLibrary.ts +35 -0
- package/src/browseNpmLibrary.ts +367 -0
- package/src/cli.ts +105 -0
- package/src/index.ts +288 -0
- package/tsconfig.json +21 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
3
|
+
import { join as join3, basename, relative } from "path";
|
|
4
|
+
|
|
5
|
+
// src/browseLocalLibrary.ts
|
|
6
|
+
import { resolve, join } from "path";
|
|
7
|
+
import { existsSync } from "fs";
|
|
8
|
+
function browseLocalLibrary(targetPath) {
|
|
9
|
+
const resolvedPath = resolve(targetPath);
|
|
10
|
+
const docsPath = join(resolvedPath, "docs");
|
|
11
|
+
const hasDocsFolder = existsSync(docsPath);
|
|
12
|
+
const dirs = [resolvedPath];
|
|
13
|
+
if (hasDocsFolder) {
|
|
14
|
+
dirs.push(docsPath);
|
|
15
|
+
}
|
|
16
|
+
const helper = new DocFilesHelper({
|
|
17
|
+
dirs,
|
|
18
|
+
overrideGetSubcommand: `show ${targetPath}`
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
libraryPath: resolvedPath,
|
|
22
|
+
helper,
|
|
23
|
+
hasDocsFolder
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/browseNpmLibrary.ts
|
|
28
|
+
import { readFileSync, readdirSync, existsSync as existsSync2, mkdirSync, writeFileSync } from "fs";
|
|
29
|
+
import { join as join2, dirname } from "path";
|
|
30
|
+
import { homedir } from "os";
|
|
31
|
+
import { runShellCommand } from "@facetlayer/subprocess-wrapper";
|
|
32
|
+
function findExactMatch(nodeModulesPath, libraryName) {
|
|
33
|
+
if (libraryName.startsWith("@")) {
|
|
34
|
+
const [scope, pkgName] = libraryName.split("/");
|
|
35
|
+
const scopePath = join2(nodeModulesPath, scope);
|
|
36
|
+
if (pkgName && existsSync2(scopePath)) {
|
|
37
|
+
const fullPath2 = join2(scopePath, pkgName);
|
|
38
|
+
if (existsSync2(fullPath2) && existsSync2(join2(fullPath2, "package.json"))) {
|
|
39
|
+
return fullPath2;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const fullPath = join2(nodeModulesPath, libraryName);
|
|
45
|
+
if (existsSync2(fullPath) && existsSync2(join2(fullPath, "package.json"))) {
|
|
46
|
+
return fullPath;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function findPartialMatches(nodeModulesPath, partialName) {
|
|
51
|
+
const matches = [];
|
|
52
|
+
const lowerPartial = partialName.toLowerCase();
|
|
53
|
+
if (!existsSync2(nodeModulesPath)) {
|
|
54
|
+
return matches;
|
|
55
|
+
}
|
|
56
|
+
const entries = readdirSync(nodeModulesPath, { withFileTypes: true });
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (!entry.isDirectory()) continue;
|
|
59
|
+
if (entry.name.startsWith("@")) {
|
|
60
|
+
const scopePath = join2(nodeModulesPath, entry.name);
|
|
61
|
+
const scopedEntries = readdirSync(scopePath, { withFileTypes: true });
|
|
62
|
+
for (const scopedEntry of scopedEntries) {
|
|
63
|
+
if (!scopedEntry.isDirectory()) continue;
|
|
64
|
+
const fullName = `${entry.name}/${scopedEntry.name}`;
|
|
65
|
+
if (fullName.toLowerCase().includes(lowerPartial)) {
|
|
66
|
+
const fullPath = join2(scopePath, scopedEntry.name);
|
|
67
|
+
if (existsSync2(join2(fullPath, "package.json"))) {
|
|
68
|
+
matches.push({
|
|
69
|
+
libraryPath: fullPath,
|
|
70
|
+
libraryName: fullName,
|
|
71
|
+
matchType: "partial"
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
if (entry.name.toLowerCase().includes(lowerPartial)) {
|
|
78
|
+
const fullPath = join2(nodeModulesPath, entry.name);
|
|
79
|
+
if (existsSync2(join2(fullPath, "package.json"))) {
|
|
80
|
+
matches.push({
|
|
81
|
+
libraryPath: fullPath,
|
|
82
|
+
libraryName: entry.name,
|
|
83
|
+
matchType: "partial"
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return matches;
|
|
90
|
+
}
|
|
91
|
+
function getNodeModulesPaths(startDir) {
|
|
92
|
+
const paths = [];
|
|
93
|
+
let currentDir = startDir;
|
|
94
|
+
while (true) {
|
|
95
|
+
const nodeModulesPath = join2(currentDir, "node_modules");
|
|
96
|
+
if (existsSync2(nodeModulesPath)) {
|
|
97
|
+
paths.push(nodeModulesPath);
|
|
98
|
+
}
|
|
99
|
+
const parentDir = dirname(currentDir);
|
|
100
|
+
if (parentDir === currentDir) {
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
currentDir = parentDir;
|
|
104
|
+
}
|
|
105
|
+
return paths;
|
|
106
|
+
}
|
|
107
|
+
function findLibraryInNodeModules(libraryName, startDir) {
|
|
108
|
+
const cwd = startDir || process.cwd();
|
|
109
|
+
const nodeModulesPaths = getNodeModulesPaths(cwd);
|
|
110
|
+
for (const nodeModulesPath of nodeModulesPaths) {
|
|
111
|
+
const exactPath = findExactMatch(nodeModulesPath, libraryName);
|
|
112
|
+
if (exactPath) {
|
|
113
|
+
return {
|
|
114
|
+
libraryPath: exactPath,
|
|
115
|
+
libraryName,
|
|
116
|
+
matchType: "exact"
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const nodeModulesPath of nodeModulesPaths) {
|
|
121
|
+
const partialMatches = findPartialMatches(nodeModulesPath, libraryName);
|
|
122
|
+
if (partialMatches.length === 1) {
|
|
123
|
+
return partialMatches[0];
|
|
124
|
+
}
|
|
125
|
+
if (partialMatches.length > 1) {
|
|
126
|
+
console.warn(`Multiple partial matches found for "${libraryName}":`);
|
|
127
|
+
for (const match of partialMatches) {
|
|
128
|
+
console.warn(` - ${match.libraryName}`);
|
|
129
|
+
}
|
|
130
|
+
console.warn(`Using: ${partialMatches[0].libraryName}`);
|
|
131
|
+
return partialMatches[0];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
function getInstallationDirectory() {
|
|
137
|
+
const stateDir = join2(homedir(), ".cache", "docs-tool");
|
|
138
|
+
const installDir = join2(stateDir, "installed-packages");
|
|
139
|
+
if (!existsSync2(installDir)) {
|
|
140
|
+
mkdirSync(installDir, { recursive: true });
|
|
141
|
+
}
|
|
142
|
+
return installDir;
|
|
143
|
+
}
|
|
144
|
+
function ensureInstallDirInitialized(installDir) {
|
|
145
|
+
const packageJsonPath = join2(installDir, "package.json");
|
|
146
|
+
if (!existsSync2(packageJsonPath)) {
|
|
147
|
+
const packageJson = {
|
|
148
|
+
name: "docs-tool-installed-packages",
|
|
149
|
+
version: "1.0.0",
|
|
150
|
+
private: true,
|
|
151
|
+
description: "Packages installed by docs-tool for documentation viewing"
|
|
152
|
+
};
|
|
153
|
+
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function findInInstallDir(installDir, libraryName) {
|
|
157
|
+
const nodeModulesPath = join2(installDir, "node_modules");
|
|
158
|
+
if (!existsSync2(nodeModulesPath)) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const exactPath = findExactMatch(nodeModulesPath, libraryName);
|
|
162
|
+
if (exactPath) {
|
|
163
|
+
return {
|
|
164
|
+
libraryPath: exactPath,
|
|
165
|
+
libraryName,
|
|
166
|
+
matchType: "exact"
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const partialMatches = findPartialMatches(nodeModulesPath, libraryName);
|
|
170
|
+
if (partialMatches.length >= 1) {
|
|
171
|
+
return partialMatches[0];
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
async function getLatestVersion(libraryName) {
|
|
176
|
+
var _a;
|
|
177
|
+
try {
|
|
178
|
+
const result = await runShellCommand("npm", ["view", libraryName, "version"]);
|
|
179
|
+
if (result.failed() || !result.stdout) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
return ((_a = result.stdout[0]) == null ? void 0 : _a.trim()) || null;
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function getInstalledVersion(libraryPath) {
|
|
188
|
+
try {
|
|
189
|
+
const packageJsonPath = join2(libraryPath, "package.json");
|
|
190
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
191
|
+
return packageJson.version || null;
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function installLibrary(installDir, libraryName) {
|
|
197
|
+
ensureInstallDirInitialized(installDir);
|
|
198
|
+
console.log(`Installing ${libraryName}...`);
|
|
199
|
+
const result = await runShellCommand("npm", ["install", libraryName, "--ignore-scripts"], {
|
|
200
|
+
cwd: installDir
|
|
201
|
+
});
|
|
202
|
+
if (result.failed()) {
|
|
203
|
+
throw new Error(`Failed to install ${libraryName}: ${result.stderrAsString()}`);
|
|
204
|
+
}
|
|
205
|
+
console.log(`Successfully installed ${libraryName}`);
|
|
206
|
+
}
|
|
207
|
+
async function updateLibrary(installDir, libraryName) {
|
|
208
|
+
console.log(`Updating ${libraryName} to latest version...`);
|
|
209
|
+
const result = await runShellCommand("npm", ["update", libraryName, "--ignore-scripts"], {
|
|
210
|
+
cwd: installDir
|
|
211
|
+
});
|
|
212
|
+
if (result.failed()) {
|
|
213
|
+
console.warn(`Warning: Failed to update ${libraryName}: ${result.stderrAsString()}`);
|
|
214
|
+
} else {
|
|
215
|
+
console.log(`Successfully updated ${libraryName}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function findLibrary(libraryName, options) {
|
|
219
|
+
const localResult = findLibraryInNodeModules(libraryName);
|
|
220
|
+
if (localResult) {
|
|
221
|
+
return localResult;
|
|
222
|
+
}
|
|
223
|
+
if (options == null ? void 0 : options.skipInstall) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const installDir = getInstallationDirectory();
|
|
227
|
+
let installedResult = findInInstallDir(installDir, libraryName);
|
|
228
|
+
if (installedResult) {
|
|
229
|
+
const installedVersion = getInstalledVersion(installedResult.libraryPath);
|
|
230
|
+
const latestVersion = await getLatestVersion(installedResult.libraryName);
|
|
231
|
+
if (installedVersion && latestVersion && installedVersion !== latestVersion) {
|
|
232
|
+
console.log(`Found ${installedResult.libraryName}@${installedVersion}, latest is ${latestVersion}`);
|
|
233
|
+
await updateLibrary(installDir, installedResult.libraryName);
|
|
234
|
+
installedResult = findInInstallDir(installDir, libraryName);
|
|
235
|
+
}
|
|
236
|
+
return installedResult;
|
|
237
|
+
}
|
|
238
|
+
await installLibrary(installDir, libraryName);
|
|
239
|
+
return findInInstallDir(installDir, libraryName);
|
|
240
|
+
}
|
|
241
|
+
function getLibraryDocs(libraryPath, libraryName) {
|
|
242
|
+
const dirs = [];
|
|
243
|
+
const files = [];
|
|
244
|
+
const readmePath = join2(libraryPath, "README.md");
|
|
245
|
+
const docsPath = join2(libraryPath, "docs");
|
|
246
|
+
const hasReadme = existsSync2(readmePath);
|
|
247
|
+
const hasDocsFolder = existsSync2(docsPath);
|
|
248
|
+
if (hasReadme) {
|
|
249
|
+
files.push(readmePath);
|
|
250
|
+
}
|
|
251
|
+
if (hasDocsFolder) {
|
|
252
|
+
dirs.push(docsPath);
|
|
253
|
+
}
|
|
254
|
+
const helper = new DocFilesHelper({
|
|
255
|
+
dirs,
|
|
256
|
+
files,
|
|
257
|
+
overrideGetSubcommand: `show ${libraryName}`
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
libraryName,
|
|
261
|
+
libraryPath,
|
|
262
|
+
helper,
|
|
263
|
+
hasReadme,
|
|
264
|
+
hasDocsFolder
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async function browseNpmLibrary(libraryName, options) {
|
|
268
|
+
const location = await findLibrary(libraryName, options);
|
|
269
|
+
if (!location) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
return getLibraryDocs(location.libraryPath, location.libraryName);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/index.ts
|
|
276
|
+
function parseFrontmatter(text) {
|
|
277
|
+
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
278
|
+
const match = text.match(frontmatterRegex);
|
|
279
|
+
if (!match) {
|
|
280
|
+
return {
|
|
281
|
+
frontmatter: {},
|
|
282
|
+
content: text
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const [, frontmatterBlock, content] = match;
|
|
286
|
+
const frontmatter = {};
|
|
287
|
+
for (const line of frontmatterBlock.split("\n")) {
|
|
288
|
+
const colonIndex = line.indexOf(":");
|
|
289
|
+
if (colonIndex === -1) continue;
|
|
290
|
+
const key = line.slice(0, colonIndex).trim();
|
|
291
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
292
|
+
frontmatter[key] = value;
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
frontmatter,
|
|
296
|
+
content: content.trim()
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
var DocFilesHelper = class {
|
|
300
|
+
constructor(options) {
|
|
301
|
+
this.options = options;
|
|
302
|
+
this.fileMap = /* @__PURE__ */ new Map();
|
|
303
|
+
if (options.dirs) {
|
|
304
|
+
for (const dir of options.dirs) {
|
|
305
|
+
const files = readdirSync2(dir);
|
|
306
|
+
for (const file of files) {
|
|
307
|
+
if (!file.endsWith(".md")) continue;
|
|
308
|
+
this.fileMap.set(file, join3(dir, file));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (options.files) {
|
|
313
|
+
for (const filePath of options.files) {
|
|
314
|
+
const baseFilename = basename(filePath);
|
|
315
|
+
this.fileMap.set(baseFilename, filePath);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
formatGetDocCommand(filename) {
|
|
320
|
+
const script = relative(process.cwd(), process.argv[1]);
|
|
321
|
+
const binName = basename(script);
|
|
322
|
+
const subcommand = this.options.overrideGetSubcommand || "show";
|
|
323
|
+
if (binName === "." || binName.endsWith(".js") || binName.endsWith(".mjs") || binName.endsWith(".ts")) {
|
|
324
|
+
return `node ${script} ${subcommand} ${filename}`;
|
|
325
|
+
}
|
|
326
|
+
return `${binName} ${subcommand} ${filename}`;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* List all doc files, returning their metadata from frontmatter.
|
|
330
|
+
* Files that don't exist are silently skipped.
|
|
331
|
+
*/
|
|
332
|
+
listDocs() {
|
|
333
|
+
const docs = [];
|
|
334
|
+
for (const [baseFilename, fullPath] of this.fileMap) {
|
|
335
|
+
let rawContent;
|
|
336
|
+
try {
|
|
337
|
+
rawContent = readFileSync2(fullPath, "utf-8");
|
|
338
|
+
} catch (err) {
|
|
339
|
+
if (err.code === "ENOENT") {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
throw err;
|
|
343
|
+
}
|
|
344
|
+
const { frontmatter } = parseFrontmatter(rawContent);
|
|
345
|
+
docs.push({
|
|
346
|
+
name: frontmatter.name || basename(baseFilename, ".md"),
|
|
347
|
+
description: frontmatter.description || "",
|
|
348
|
+
filename: baseFilename
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
return docs;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Get the contents of a specific doc file by name.
|
|
355
|
+
* If the exact filename doesn't exist, looks for a partial match.
|
|
356
|
+
* Throws an error if the doc file is not found or if multiple matches are found.
|
|
357
|
+
*/
|
|
358
|
+
getDoc(name) {
|
|
359
|
+
const baseName = name.endsWith(".md") ? name.slice(0, -3) : name;
|
|
360
|
+
const filename = `${baseName}.md`;
|
|
361
|
+
const fullPath = this.fileMap.get(filename);
|
|
362
|
+
if (fullPath) {
|
|
363
|
+
const rawContent2 = readFileSync2(fullPath, "utf-8");
|
|
364
|
+
const { frontmatter: frontmatter2, content: content2 } = parseFrontmatter(rawContent2);
|
|
365
|
+
return {
|
|
366
|
+
name: frontmatter2.name || baseName,
|
|
367
|
+
description: frontmatter2.description || "",
|
|
368
|
+
filename,
|
|
369
|
+
content: content2,
|
|
370
|
+
rawContent: rawContent2,
|
|
371
|
+
fullPath
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
const docs = this.listDocs();
|
|
375
|
+
const matches = docs.filter(
|
|
376
|
+
(doc) => doc.filename.toLowerCase().includes(baseName.toLowerCase()) || doc.name.toLowerCase().includes(baseName.toLowerCase())
|
|
377
|
+
);
|
|
378
|
+
if (matches.length === 0) {
|
|
379
|
+
throw new Error(`Doc file not found: ${baseName}`);
|
|
380
|
+
}
|
|
381
|
+
if (matches.length > 1) {
|
|
382
|
+
const matchNames = matches.map((m) => m.filename).join(", ");
|
|
383
|
+
throw new Error(
|
|
384
|
+
`Multiple docs match "${baseName}": ${matchNames}. Please be more specific.`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
const matchedFilename = matches[0].filename;
|
|
388
|
+
const matchedPath = this.fileMap.get(matchedFilename);
|
|
389
|
+
const rawContent = readFileSync2(matchedPath, "utf-8");
|
|
390
|
+
const { frontmatter, content } = parseFrontmatter(rawContent);
|
|
391
|
+
return {
|
|
392
|
+
name: frontmatter.name || basename(matchedFilename, ".md"),
|
|
393
|
+
description: frontmatter.description || "",
|
|
394
|
+
filename: matchedFilename,
|
|
395
|
+
content,
|
|
396
|
+
rawContent,
|
|
397
|
+
fullPath: matchedPath
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Print a formatted list of all doc files to stdout.
|
|
402
|
+
* Used by the 'list-docs' command.
|
|
403
|
+
*/
|
|
404
|
+
printDocFileList() {
|
|
405
|
+
const docs = this.listDocs();
|
|
406
|
+
console.log("Available doc files:\n");
|
|
407
|
+
for (const doc of docs) {
|
|
408
|
+
if (doc.description) {
|
|
409
|
+
console.log(` ${doc.name} (${this.formatGetDocCommand(doc.filename)}):`);
|
|
410
|
+
console.log(` ${doc.description}
|
|
411
|
+
`);
|
|
412
|
+
} else {
|
|
413
|
+
console.log(` ${doc.name} (${this.formatGetDocCommand(doc.filename)})
|
|
414
|
+
`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Print the raw contents of a specific doc file to stdout.
|
|
420
|
+
*
|
|
421
|
+
* Used by the 'get-doc' command.
|
|
422
|
+
*/
|
|
423
|
+
printDocFileContents(name) {
|
|
424
|
+
try {
|
|
425
|
+
const doc = this.getDoc(name);
|
|
426
|
+
console.log(doc.rawContent);
|
|
427
|
+
console.log(`
|
|
428
|
+
(File source: ${doc.fullPath})`);
|
|
429
|
+
} catch {
|
|
430
|
+
console.error(`Doc file not found: ${name}`);
|
|
431
|
+
console.error('Run with "list-docs" or "list" command to see available docs.');
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
yargsSetup(yargs) {
|
|
436
|
+
yargs.command(
|
|
437
|
+
"list-docs",
|
|
438
|
+
"List available documentation files",
|
|
439
|
+
{},
|
|
440
|
+
async () => this.printDocFileList()
|
|
441
|
+
).command(
|
|
442
|
+
"get-doc <name>",
|
|
443
|
+
"Display the contents of a documentation file",
|
|
444
|
+
(yargs2) => {
|
|
445
|
+
return yargs2.positional("name", {
|
|
446
|
+
type: "string",
|
|
447
|
+
describe: "Name of the doc file",
|
|
448
|
+
demandOption: true
|
|
449
|
+
});
|
|
450
|
+
},
|
|
451
|
+
async (argv) => this.printDocFileContents(argv.name)
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
function parseTarget(target) {
|
|
456
|
+
if (target.startsWith(".") || target.startsWith("/")) {
|
|
457
|
+
return { type: "directory", value: target };
|
|
458
|
+
}
|
|
459
|
+
return { type: "npm", value: target };
|
|
460
|
+
}
|
|
461
|
+
export {
|
|
462
|
+
DocFilesHelper,
|
|
463
|
+
browseLocalLibrary,
|
|
464
|
+
browseNpmLibrary,
|
|
465
|
+
findLibrary,
|
|
466
|
+
findLibraryInNodeModules,
|
|
467
|
+
getInstallationDirectory,
|
|
468
|
+
getLibraryDocs,
|
|
469
|
+
parseFrontmatter,
|
|
470
|
+
parseTarget
|
|
471
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: project-setup
|
|
3
|
+
description: Instructions for adding docs-tool to your CLI application
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Project Setup
|
|
7
|
+
|
|
8
|
+
This guide explains how to add `@facetlayer/docs-tool` to your CLI application.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add @facetlayer/docs-tool
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Setting Up the Doc Files Folder
|
|
17
|
+
|
|
18
|
+
Create a `docs` directory in your project root to store your documentation files:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
my-project/
|
|
22
|
+
├── docs/
|
|
23
|
+
│ ├── getting-started.md
|
|
24
|
+
│ └── configuration.md
|
|
25
|
+
├── src/
|
|
26
|
+
│ └── cli.ts
|
|
27
|
+
└── package.json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Each doc file should include YAML frontmatter with `name` and `description`:
|
|
31
|
+
|
|
32
|
+
```markdown
|
|
33
|
+
---
|
|
34
|
+
name: getting-started
|
|
35
|
+
description: Quick start guide for new users
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
# Getting Started
|
|
39
|
+
|
|
40
|
+
Your markdown content here.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Creating a DocFilesHelper Instance
|
|
44
|
+
|
|
45
|
+
In your CLI script (e.g., `src/cli.ts`), create a `DocFilesHelper` instance:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { DocFilesHelper } from '@facetlayer/docs-tool';
|
|
49
|
+
import { join, dirname } from 'path';
|
|
50
|
+
import { fileURLToPath } from 'url';
|
|
51
|
+
|
|
52
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
53
|
+
const __dirname = dirname(__filename);
|
|
54
|
+
const __packageRoot = join(__dirname, '..');
|
|
55
|
+
|
|
56
|
+
const docFiles = new DocFilesHelper({
|
|
57
|
+
dirs: [join(__packageRoot, 'docs')],
|
|
58
|
+
files: [join(__packageRoot, 'README.md')],
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Configuration Options
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
interface DocFilesHelperOptions {
|
|
66
|
+
// List of directories to search for *.md files
|
|
67
|
+
dirs?: string[];
|
|
68
|
+
|
|
69
|
+
// List of specific files to include
|
|
70
|
+
files?: string[];
|
|
71
|
+
|
|
72
|
+
// Override the subcommand name for getting a single doc (default: 'get-doc')
|
|
73
|
+
overrideGetSubcommand?: string;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Adding Commands to Yargs
|
|
78
|
+
|
|
79
|
+
There are two approaches depending on how your CLI is structured.
|
|
80
|
+
|
|
81
|
+
### Option 1: Using yargsSetup() with parse()
|
|
82
|
+
|
|
83
|
+
If your CLI uses yargs with `.parse()` and command handlers, call `yargsSetup()`:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import yargs from 'yargs';
|
|
87
|
+
import { hideBin } from 'yargs/helpers';
|
|
88
|
+
import { DocFilesHelper } from '@facetlayer/docs-tool';
|
|
89
|
+
|
|
90
|
+
const docFiles = new DocFilesHelper({
|
|
91
|
+
dirs: [join(__packageRoot, 'docs')],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
async function main() {
|
|
95
|
+
const args = yargs(hideBin(process.argv))
|
|
96
|
+
.command(
|
|
97
|
+
'my-command',
|
|
98
|
+
'Description of my command',
|
|
99
|
+
{},
|
|
100
|
+
async () => {
|
|
101
|
+
// Your command implementation
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Add list-docs and get-doc commands
|
|
106
|
+
docFiles.yargsSetup(args);
|
|
107
|
+
|
|
108
|
+
args
|
|
109
|
+
.strictCommands()
|
|
110
|
+
.demandCommand(1, 'You must specify a command')
|
|
111
|
+
.help()
|
|
112
|
+
.parse(); // Must use parse(), not parseSync()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
main();
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Important:** `yargsSetup()` registers async command handlers. You must use `.parse()` instead of `.parseSync()`, otherwise yargs will throw an error.
|
|
119
|
+
|
|
120
|
+
### Option 2: Manual registration with parseSync()
|
|
121
|
+
|
|
122
|
+
If your CLI uses `.parseSync()` with a switch statement to handle commands, register the commands manually for help text and handle them in your switch:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import yargs from 'yargs';
|
|
126
|
+
import { hideBin } from 'yargs/helpers';
|
|
127
|
+
import { DocFilesHelper } from '@facetlayer/docs-tool';
|
|
128
|
+
|
|
129
|
+
const docFiles = new DocFilesHelper({
|
|
130
|
+
dirs: [join(__packageRoot, 'docs')],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
function configureYargs() {
|
|
134
|
+
return yargs(hideBin(process.argv))
|
|
135
|
+
.command('my-command', 'Description of my command', () => {})
|
|
136
|
+
// Register doc commands for help text (no handlers)
|
|
137
|
+
.command('list-docs', 'List available documentation files', () => {})
|
|
138
|
+
.command('get-doc <name>', 'Display contents of a doc file', () => {})
|
|
139
|
+
.help();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function main() {
|
|
143
|
+
const argv = configureYargs().parseSync();
|
|
144
|
+
const command = argv._[0] as string;
|
|
145
|
+
const name = argv.name as string;
|
|
146
|
+
|
|
147
|
+
switch (command) {
|
|
148
|
+
case 'my-command':
|
|
149
|
+
// handle my-command
|
|
150
|
+
break;
|
|
151
|
+
|
|
152
|
+
case 'list-docs':
|
|
153
|
+
docFiles.printDocFileList();
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case 'get-doc':
|
|
157
|
+
docFiles.printDocFileContents(name);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
main();
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
This adds two commands to your CLI:
|
|
166
|
+
|
|
167
|
+
- `<app> list-docs` - List all available doc files with descriptions
|
|
168
|
+
- `<app> get-doc <name>` - Display the contents of a specific doc file
|
|
169
|
+
|
|
170
|
+
## Manual Usage (Without Yargs)
|
|
171
|
+
|
|
172
|
+
If you're not using Yargs or want more control, use the helper methods directly:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// List all docs
|
|
176
|
+
const docs = docFiles.listDocs();
|
|
177
|
+
// Returns: [{ name, description, filename }, ...]
|
|
178
|
+
|
|
179
|
+
// Get a specific doc
|
|
180
|
+
const doc = docFiles.getDoc('getting-started');
|
|
181
|
+
// Returns: { name, description, filename, content, rawContent, fullPath }
|
|
182
|
+
|
|
183
|
+
// Print formatted list to stdout
|
|
184
|
+
docFiles.printDocFileList();
|
|
185
|
+
|
|
186
|
+
// Print doc contents to stdout
|
|
187
|
+
docFiles.printDocFileContents('getting-started');
|
|
188
|
+
```
|