@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/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
+ ```