@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.
@@ -0,0 +1,367 @@
1
+ import { readFileSync, readdirSync, existsSync, mkdirSync, writeFileSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { homedir } from 'os';
4
+ import { runShellCommand } from '@facetlayer/subprocess-wrapper';
5
+ import { DocFilesHelper } from './index.ts';
6
+
7
+ export interface LibraryLocation {
8
+ libraryPath: string;
9
+ libraryName: string;
10
+ matchType: 'exact' | 'partial';
11
+ }
12
+
13
+ export interface NpmLibraryDocs {
14
+ libraryName: string;
15
+ libraryPath: string;
16
+ helper: DocFilesHelper;
17
+ hasReadme: boolean;
18
+ hasDocsFolder: boolean;
19
+ }
20
+
21
+ /**
22
+ * Check if a directory contains a package that matches the given name exactly
23
+ */
24
+ function findExactMatch(nodeModulesPath: string, libraryName: string): string | null {
25
+ // Handle scoped packages like @scope/package
26
+ if (libraryName.startsWith('@')) {
27
+ const [scope, pkgName] = libraryName.split('/');
28
+ const scopePath = join(nodeModulesPath, scope);
29
+ if (pkgName && existsSync(scopePath)) {
30
+ const fullPath = join(scopePath, pkgName);
31
+ if (existsSync(fullPath) && existsSync(join(fullPath, 'package.json'))) {
32
+ return fullPath;
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+
38
+ // Regular package
39
+ const fullPath = join(nodeModulesPath, libraryName);
40
+ if (existsSync(fullPath) && existsSync(join(fullPath, 'package.json'))) {
41
+ return fullPath;
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * Find packages that partially match the given name
48
+ */
49
+ function findPartialMatches(nodeModulesPath: string, partialName: string): LibraryLocation[] {
50
+ const matches: LibraryLocation[] = [];
51
+ const lowerPartial = partialName.toLowerCase();
52
+
53
+ if (!existsSync(nodeModulesPath)) {
54
+ return matches;
55
+ }
56
+
57
+ const entries = readdirSync(nodeModulesPath, { withFileTypes: true });
58
+
59
+ for (const entry of entries) {
60
+ if (!entry.isDirectory()) continue;
61
+
62
+ // Handle scoped packages
63
+ if (entry.name.startsWith('@')) {
64
+ const scopePath = join(nodeModulesPath, entry.name);
65
+ const scopedEntries = readdirSync(scopePath, { withFileTypes: true });
66
+ for (const scopedEntry of scopedEntries) {
67
+ if (!scopedEntry.isDirectory()) continue;
68
+ const fullName = `${entry.name}/${scopedEntry.name}`;
69
+ if (fullName.toLowerCase().includes(lowerPartial)) {
70
+ const fullPath = join(scopePath, scopedEntry.name);
71
+ if (existsSync(join(fullPath, 'package.json'))) {
72
+ matches.push({
73
+ libraryPath: fullPath,
74
+ libraryName: fullName,
75
+ matchType: 'partial',
76
+ });
77
+ }
78
+ }
79
+ }
80
+ } else {
81
+ // Regular package
82
+ if (entry.name.toLowerCase().includes(lowerPartial)) {
83
+ const fullPath = join(nodeModulesPath, entry.name);
84
+ if (existsSync(join(fullPath, 'package.json'))) {
85
+ matches.push({
86
+ libraryPath: fullPath,
87
+ libraryName: entry.name,
88
+ matchType: 'partial',
89
+ });
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ return matches;
96
+ }
97
+
98
+ /**
99
+ * Get all node_modules directories from current directory up to root
100
+ */
101
+ function getNodeModulesPaths(startDir: string): string[] {
102
+ const paths: string[] = [];
103
+ let currentDir = startDir;
104
+
105
+ while (true) {
106
+ const nodeModulesPath = join(currentDir, 'node_modules');
107
+ if (existsSync(nodeModulesPath)) {
108
+ paths.push(nodeModulesPath);
109
+ }
110
+
111
+ const parentDir = dirname(currentDir);
112
+ if (parentDir === currentDir) {
113
+ // Reached root
114
+ break;
115
+ }
116
+ currentDir = parentDir;
117
+ }
118
+
119
+ return paths;
120
+ }
121
+
122
+ /**
123
+ * Find a library by name in node_modules directories.
124
+ */
125
+ export function findLibraryInNodeModules(libraryName: string, startDir?: string): LibraryLocation | null {
126
+ const cwd = startDir || process.cwd();
127
+ const nodeModulesPaths = getNodeModulesPaths(cwd);
128
+
129
+ // Phase 1: Try exact match in all directories first
130
+ for (const nodeModulesPath of nodeModulesPaths) {
131
+ const exactPath = findExactMatch(nodeModulesPath, libraryName);
132
+ if (exactPath) {
133
+ return {
134
+ libraryPath: exactPath,
135
+ libraryName: libraryName,
136
+ matchType: 'exact',
137
+ };
138
+ }
139
+ }
140
+
141
+ // Phase 2: Try partial match in all directories
142
+ for (const nodeModulesPath of nodeModulesPaths) {
143
+ const partialMatches = findPartialMatches(nodeModulesPath, libraryName);
144
+ if (partialMatches.length === 1) {
145
+ return partialMatches[0];
146
+ }
147
+ if (partialMatches.length > 1) {
148
+ console.warn(`Multiple partial matches found for "${libraryName}":`);
149
+ for (const match of partialMatches) {
150
+ console.warn(` - ${match.libraryName}`);
151
+ }
152
+ console.warn(`Using: ${partialMatches[0].libraryName}`);
153
+ return partialMatches[0];
154
+ }
155
+ }
156
+
157
+ return null;
158
+ }
159
+
160
+ /**
161
+ * Get the installation directory for libraries that aren't found in node_modules
162
+ */
163
+ export function getInstallationDirectory(): string {
164
+ const stateDir = join(homedir(), '.cache', 'docs-tool');
165
+ const installDir = join(stateDir, 'installed-packages');
166
+
167
+ if (!existsSync(installDir)) {
168
+ mkdirSync(installDir, { recursive: true });
169
+ }
170
+
171
+ return installDir;
172
+ }
173
+
174
+ /**
175
+ * Initialize the installation directory with a package.json if needed
176
+ */
177
+ function ensureInstallDirInitialized(installDir: string): void {
178
+ const packageJsonPath = join(installDir, 'package.json');
179
+
180
+ if (!existsSync(packageJsonPath)) {
181
+ const packageJson = {
182
+ name: 'docs-tool-installed-packages',
183
+ version: '1.0.0',
184
+ private: true,
185
+ description: 'Packages installed by docs-tool for documentation viewing',
186
+ };
187
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Check if a library is installed in our installation directory
193
+ */
194
+ function findInInstallDir(installDir: string, libraryName: string): LibraryLocation | null {
195
+ const nodeModulesPath = join(installDir, 'node_modules');
196
+
197
+ if (!existsSync(nodeModulesPath)) {
198
+ return null;
199
+ }
200
+
201
+ const exactPath = findExactMatch(nodeModulesPath, libraryName);
202
+ if (exactPath) {
203
+ return {
204
+ libraryPath: exactPath,
205
+ libraryName: libraryName,
206
+ matchType: 'exact',
207
+ };
208
+ }
209
+
210
+ const partialMatches = findPartialMatches(nodeModulesPath, libraryName);
211
+ if (partialMatches.length >= 1) {
212
+ return partialMatches[0];
213
+ }
214
+
215
+ return null;
216
+ }
217
+
218
+ /**
219
+ * Get the latest version of a package from npm registry
220
+ */
221
+ async function getLatestVersion(libraryName: string): Promise<string | null> {
222
+ try {
223
+ const result = await runShellCommand('npm', ['view', libraryName, 'version']);
224
+ if (result.failed() || !result.stdout) {
225
+ return null;
226
+ }
227
+ return result.stdout[0]?.trim() || null;
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Get the installed version of a package
235
+ */
236
+ function getInstalledVersion(libraryPath: string): string | null {
237
+ try {
238
+ const packageJsonPath = join(libraryPath, 'package.json');
239
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
240
+ return packageJson.version || null;
241
+ } catch {
242
+ return null;
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Install a library using npm (without running install scripts)
248
+ */
249
+ async function installLibrary(installDir: string, libraryName: string): Promise<void> {
250
+ ensureInstallDirInitialized(installDir);
251
+
252
+ console.log(`Installing ${libraryName}...`);
253
+
254
+ const result = await runShellCommand('npm', ['install', libraryName, '--ignore-scripts'], {
255
+ cwd: installDir,
256
+ });
257
+
258
+ if (result.failed()) {
259
+ throw new Error(`Failed to install ${libraryName}: ${result.stderrAsString()}`);
260
+ }
261
+ console.log(`Successfully installed ${libraryName}`);
262
+ }
263
+
264
+ /**
265
+ * Update a library to the latest version
266
+ */
267
+ async function updateLibrary(installDir: string, libraryName: string): Promise<void> {
268
+ console.log(`Updating ${libraryName} to latest version...`);
269
+
270
+ const result = await runShellCommand('npm', ['update', libraryName, '--ignore-scripts'], {
271
+ cwd: installDir,
272
+ });
273
+
274
+ if (result.failed()) {
275
+ console.warn(`Warning: Failed to update ${libraryName}: ${result.stderrAsString()}`);
276
+ } else {
277
+ console.log(`Successfully updated ${libraryName}`);
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Find a library, installing it if necessary
283
+ */
284
+ export async function findLibrary(libraryName: string, options?: { skipInstall?: boolean }): Promise<LibraryLocation | null> {
285
+ // First, try to find in local node_modules
286
+ const localResult = findLibraryInNodeModules(libraryName);
287
+ if (localResult) {
288
+ return localResult;
289
+ }
290
+
291
+ if (options?.skipInstall) {
292
+ return null;
293
+ }
294
+
295
+ // Check our installation directory
296
+ const installDir = getInstallationDirectory();
297
+ let installedResult = findInInstallDir(installDir, libraryName);
298
+
299
+ if (installedResult) {
300
+ // Check if we need to update to a newer version
301
+ const installedVersion = getInstalledVersion(installedResult.libraryPath);
302
+ const latestVersion = await getLatestVersion(installedResult.libraryName);
303
+
304
+ if (installedVersion && latestVersion && installedVersion !== latestVersion) {
305
+ console.log(`Found ${installedResult.libraryName}@${installedVersion}, latest is ${latestVersion}`);
306
+ await updateLibrary(installDir, installedResult.libraryName);
307
+ installedResult = findInInstallDir(installDir, libraryName);
308
+ }
309
+
310
+ return installedResult;
311
+ }
312
+
313
+ // Not found anywhere - install it
314
+ await installLibrary(installDir, libraryName);
315
+
316
+ return findInInstallDir(installDir, libraryName);
317
+ }
318
+
319
+ /**
320
+ * Create a DocFilesHelper for a library's documentation
321
+ */
322
+ export function getLibraryDocs(libraryPath: string, libraryName: string): NpmLibraryDocs {
323
+ const dirs: string[] = [];
324
+ const files: string[] = [];
325
+
326
+ const readmePath = join(libraryPath, 'README.md');
327
+ const docsPath = join(libraryPath, 'docs');
328
+
329
+ const hasReadme = existsSync(readmePath);
330
+ const hasDocsFolder = existsSync(docsPath);
331
+
332
+ if (hasReadme) {
333
+ files.push(readmePath);
334
+ }
335
+
336
+ if (hasDocsFolder) {
337
+ dirs.push(docsPath);
338
+ }
339
+
340
+ const helper = new DocFilesHelper({
341
+ dirs,
342
+ files,
343
+ overrideGetSubcommand: `show ${libraryName}`,
344
+ });
345
+
346
+ return {
347
+ libraryName,
348
+ libraryPath,
349
+ helper,
350
+ hasReadme,
351
+ hasDocsFolder,
352
+ };
353
+ }
354
+
355
+ /**
356
+ * Browse an NPM library's documentation.
357
+ * First checks local node_modules, then installs from npm if not found.
358
+ */
359
+ export async function browseNpmLibrary(libraryName: string, options?: { skipInstall?: boolean }): Promise<NpmLibraryDocs | null> {
360
+ const location = await findLibrary(libraryName, options);
361
+
362
+ if (!location) {
363
+ return null;
364
+ }
365
+
366
+ return getLibraryDocs(location.libraryPath, location.libraryName);
367
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+
3
+ import yargs from 'yargs';
4
+ import { hideBin } from 'yargs/helpers';
5
+ import { readFileSync } from 'fs';
6
+ import { join, dirname } from 'path';
7
+ import { fileURLToPath } from 'url';
8
+ import { parseTarget, browseLocalLibrary, browseNpmLibrary } from './index.ts';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ const packageJson = JSON.parse(
13
+ readFileSync(join(__dirname, '../package.json'), 'utf-8')
14
+ );
15
+
16
+ async function main() {
17
+ await yargs(hideBin(process.argv))
18
+ .command(
19
+ 'list <target>',
20
+ 'List available doc files in a directory or NPM package',
21
+ (yargs) => {
22
+ return yargs.positional('target', {
23
+ type: 'string',
24
+ describe: 'Directory path (starts with . or /) or NPM package name',
25
+ demandOption: true,
26
+ });
27
+ },
28
+ async (argv) => {
29
+ const target = argv.target as string;
30
+ const parsed = parseTarget(target);
31
+
32
+ if (parsed.type === 'directory') {
33
+ const local = browseLocalLibrary(parsed.value);
34
+ local.helper.printDocFileList();
35
+ } else {
36
+ const docs = await browseNpmLibrary(parsed.value);
37
+ if (!docs) {
38
+ console.error(`Could not find library: ${parsed.value}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ console.log(`\nLibrary: ${docs.libraryName}`);
43
+ console.log(`Path: ${docs.libraryPath}\n`);
44
+
45
+ if (!docs.hasReadme && !docs.hasDocsFolder) {
46
+ console.log('No documentation found for this library.');
47
+ console.log('(No README.md or docs/ folder exists)');
48
+ return;
49
+ }
50
+
51
+ docs.helper.printDocFileList();
52
+ }
53
+ }
54
+ )
55
+ .command(
56
+ 'show <target> [name]',
57
+ 'Get the contents of one doc file',
58
+ (yargs) => {
59
+ return yargs
60
+ .positional('target', {
61
+ type: 'string',
62
+ describe: 'Directory path (starts with . or /) or NPM package name',
63
+ demandOption: true,
64
+ })
65
+ .positional('name', {
66
+ type: 'string',
67
+ describe: 'Name of the doc file (defaults to README)',
68
+ default: 'README',
69
+ });
70
+ },
71
+ async (argv) => {
72
+ const target = argv.target as string;
73
+ const name = argv.name as string;
74
+ const parsed = parseTarget(target);
75
+
76
+ if (parsed.type === 'directory') {
77
+ const local = browseLocalLibrary(parsed.value);
78
+ local.helper.printDocFileContents(name);
79
+ } else {
80
+ const docs = await browseNpmLibrary(parsed.value);
81
+ if (!docs) {
82
+ console.error(`Could not find library: ${parsed.value}`);
83
+ process.exit(1);
84
+ }
85
+
86
+ docs.helper.printDocFileContents(name);
87
+ }
88
+ }
89
+ )
90
+ .strictCommands()
91
+ .demandCommand(1, 'You must specify a command')
92
+ .help()
93
+ .alias('help', 'h')
94
+ .version(packageJson.version)
95
+ .alias('version', 'v')
96
+ .example([
97
+ ['$0 list ./docs', 'List all doc files in ./docs directory'],
98
+ ['$0 list lodash', 'List all doc files for the lodash NPM package'],
99
+ ['$0 show ./docs project-setup', 'Display the project-setup doc from ./docs'],
100
+ ['$0 show lodash', 'Display the README for the lodash NPM package'],
101
+ ])
102
+ .parse();
103
+ }
104
+
105
+ main();