pointfree-docs 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 +132 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +55 -0
- package/dist/commands/get.d.ts +9 -0
- package/dist/commands/get.js +30 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +63 -0
- package/dist/commands/list.d.ts +10 -0
- package/dist/commands/list.js +86 -0
- package/dist/commands/search.d.ts +10 -0
- package/dist/commands/search.js +31 -0
- package/dist/commands/stats.d.ts +8 -0
- package/dist/commands/stats.js +37 -0
- package/dist/commands/update.d.ts +8 -0
- package/dist/commands/update.js +51 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.js +121 -0
- package/dist/lib/index.d.ts +59 -0
- package/dist/lib/index.js +226 -0
- package/dist/lib/markdown.d.ts +19 -0
- package/dist/lib/markdown.js +58 -0
- package/dist/lib/repos.d.ts +20 -0
- package/dist/lib/repos.js +76 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# pf-docs
|
|
2
|
+
|
|
3
|
+
A CLI tool for searching Point-Free library documentation locally. Uses sparse git checkout and SQLite FTS5 for fast, offline full-text search. Built for use with AI coding assistants like Claude Code.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install from npm
|
|
9
|
+
npm install -g pointfree-docs
|
|
10
|
+
|
|
11
|
+
# Or install from source
|
|
12
|
+
git clone https://github.com/ronnie3786/pointfree-docs.git
|
|
13
|
+
cd pointfree-docs
|
|
14
|
+
npm install
|
|
15
|
+
npm run build
|
|
16
|
+
npm link
|
|
17
|
+
|
|
18
|
+
# See what libraries are available
|
|
19
|
+
pf-docs list --available
|
|
20
|
+
|
|
21
|
+
# Download and index the ones you use
|
|
22
|
+
pf-docs init --libs tca dependencies navigation
|
|
23
|
+
|
|
24
|
+
# Search, browse, and read
|
|
25
|
+
pf-docs search "testing effects"
|
|
26
|
+
pf-docs get tca/Articles/TestingTCA
|
|
27
|
+
pf-docs list tca
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
### `pf-docs list --available`
|
|
33
|
+
|
|
34
|
+
Show all libraries available to download.
|
|
35
|
+
|
|
36
|
+
### `pf-docs init`
|
|
37
|
+
|
|
38
|
+
Download and index documentation. Only fetches `Documentation.docc` folders via sparse checkout.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pf-docs init --libs tca dependencies navigation
|
|
42
|
+
pf-docs init --all
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### `pf-docs update`
|
|
46
|
+
|
|
47
|
+
Pull latest changes and re-index.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pf-docs update # All initialized libraries
|
|
51
|
+
pf-docs update --libs tca # Specific libraries
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `pf-docs search <query>`
|
|
55
|
+
|
|
56
|
+
Full-text search across all indexed docs.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pf-docs search "testing effects"
|
|
60
|
+
pf-docs search "navigation" --lib tca
|
|
61
|
+
pf-docs search "Store" --limit 5
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### `pf-docs get <path>`
|
|
65
|
+
|
|
66
|
+
Fetch a specific article as clean markdown.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pf-docs get tca/Articles/TestingTCA
|
|
70
|
+
pf-docs get dependencies/Articles/QuickStart --raw
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### `pf-docs list [lib]`
|
|
74
|
+
|
|
75
|
+
List indexed documentation.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pf-docs list # All indexed docs
|
|
79
|
+
pf-docs list tca --tree # Tree view for one library
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `pf-docs stats`
|
|
83
|
+
|
|
84
|
+
Show indexing statistics.
|
|
85
|
+
|
|
86
|
+
All commands support `--json` for programmatic output.
|
|
87
|
+
|
|
88
|
+
## Supported Libraries
|
|
89
|
+
|
|
90
|
+
| Short Name | Library | Description |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `tca` | swift-composable-architecture | The Composable Architecture |
|
|
93
|
+
| `dependencies` | swift-dependencies | Dependency injection library |
|
|
94
|
+
| `navigation` | swift-navigation | Navigation tools for Swift |
|
|
95
|
+
| `perception` | swift-perception | @Observable backported to iOS 16 |
|
|
96
|
+
| `sharing` | swift-sharing | Persistence & data sharing |
|
|
97
|
+
| `identified-collections` | swift-identified-collections | Identifiable-aware collections |
|
|
98
|
+
| `case-paths` | swift-case-paths | Key paths for enum cases |
|
|
99
|
+
| `custom-dump` | swift-custom-dump | Debugging/diffing tools |
|
|
100
|
+
| `concurrency-extras` | swift-concurrency-extras | Testable async/await |
|
|
101
|
+
| `clocks` | swift-clocks | Testable Swift concurrency clocks |
|
|
102
|
+
| `snapshot-testing` | swift-snapshot-testing | Snapshot testing library |
|
|
103
|
+
| `issue-reporting` | swift-issue-reporting | Runtime warnings & assertions |
|
|
104
|
+
|
|
105
|
+
Run `pf-docs list --available` to see this list in your terminal.
|
|
106
|
+
|
|
107
|
+
## Usage with Claude Code
|
|
108
|
+
|
|
109
|
+
Add the key commands to your project's `CLAUDE.md`:
|
|
110
|
+
|
|
111
|
+
```markdown
|
|
112
|
+
Use `pf-docs search "<query>"` to search Point-Free docs, `pf-docs get <path>` to read an article.
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Adding Libraries
|
|
116
|
+
|
|
117
|
+
Edit `src/config.ts` to add entries, then run `pf-docs init --libs <shortName>` to download.
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm install
|
|
123
|
+
npm run dev # Watch mode
|
|
124
|
+
npm run build # Build once
|
|
125
|
+
npm link # Install globally
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Cloned repos and the search index are stored in `data/` (gitignored).
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pf-docs CLI - Point-Free Documentation Tool
|
|
4
|
+
*
|
|
5
|
+
* A CLI for searching and browsing Point-Free library documentation.
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { initCommand } from "./commands/init.js";
|
|
9
|
+
import { updateCommand } from "./commands/update.js";
|
|
10
|
+
import { searchCommand } from "./commands/search.js";
|
|
11
|
+
import { getCommand } from "./commands/get.js";
|
|
12
|
+
import { listCommand } from "./commands/list.js";
|
|
13
|
+
import { statsCommand } from "./commands/stats.js";
|
|
14
|
+
const program = new Command();
|
|
15
|
+
program
|
|
16
|
+
.name("pf-docs")
|
|
17
|
+
.description("CLI tool for searching Point-Free library documentation")
|
|
18
|
+
.version("0.1.0");
|
|
19
|
+
program
|
|
20
|
+
.command("init")
|
|
21
|
+
.description("Initialize and download documentation for specified libraries")
|
|
22
|
+
.option("-l, --libs <libs...>", "Libraries to download (e.g., tca dependencies)")
|
|
23
|
+
.option("-a, --all", "Download all available libraries")
|
|
24
|
+
.action(initCommand);
|
|
25
|
+
program
|
|
26
|
+
.command("update")
|
|
27
|
+
.description("Update documentation from remote repositories")
|
|
28
|
+
.option("-l, --libs <libs...>", "Specific libraries to update")
|
|
29
|
+
.action(updateCommand);
|
|
30
|
+
program
|
|
31
|
+
.command("search <query>")
|
|
32
|
+
.description("Search across all indexed documentation")
|
|
33
|
+
.option("-l, --lib <lib>", "Limit search to specific library")
|
|
34
|
+
.option("-n, --limit <n>", "Max results to return", "10")
|
|
35
|
+
.option("-j, --json", "Output results as JSON")
|
|
36
|
+
.action(searchCommand);
|
|
37
|
+
program
|
|
38
|
+
.command("get <path>")
|
|
39
|
+
.description("Get a specific documentation article (e.g., tca/Testing)")
|
|
40
|
+
.option("-j, --json", "Output as JSON")
|
|
41
|
+
.option("-r, --raw", "Output raw content without header")
|
|
42
|
+
.action(getCommand);
|
|
43
|
+
program
|
|
44
|
+
.command("list [lib]")
|
|
45
|
+
.description("List available documentation articles")
|
|
46
|
+
.option("-t, --tree", "Show as tree structure")
|
|
47
|
+
.option("-j, --json", "Output as JSON")
|
|
48
|
+
.option("-a, --available", "Show all libraries available to download")
|
|
49
|
+
.action(listCommand);
|
|
50
|
+
program
|
|
51
|
+
.command("stats")
|
|
52
|
+
.description("Show indexing statistics")
|
|
53
|
+
.option("-j, --json", "Output as JSON")
|
|
54
|
+
.action(statsCommand);
|
|
55
|
+
program.parse();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* get command - Retrieve a specific documentation article
|
|
3
|
+
*/
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { getDoc, withIndex } from "../lib/index.js";
|
|
6
|
+
import { formatDocForOutput } from "../lib/markdown.js";
|
|
7
|
+
export function getCommand(path, options = {}) {
|
|
8
|
+
const doc = withIndex(() => getDoc(path));
|
|
9
|
+
if (!doc) {
|
|
10
|
+
if (options.json) {
|
|
11
|
+
console.log(JSON.stringify({ error: "Document not found", path }, null, 2));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
console.error(chalk.red(`\n✗ Document not found: ${path}`));
|
|
15
|
+
console.log(chalk.gray(`\nRun 'pf-docs list' to see available documents.`));
|
|
16
|
+
console.log(chalk.gray(`Or search: pf-docs search "<query>"`));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (options.json) {
|
|
20
|
+
console.log(JSON.stringify(doc, null, 2));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// Raw output (just content, no header)
|
|
24
|
+
if (options.raw) {
|
|
25
|
+
console.log(doc.content);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Output the formatted document (clean markdown)
|
|
29
|
+
console.log(formatDocForOutput(doc));
|
|
30
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init command - Download and index Point-Free documentation
|
|
3
|
+
*/
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { LIBRARIES, getLibrary, LIBRARY_NAMES } from "../config.js";
|
|
6
|
+
import { cloneLibrary } from "../lib/repos.js";
|
|
7
|
+
import { indexLibrary, openIndex, closeIndex } from "../lib/index.js";
|
|
8
|
+
export async function initCommand(options) {
|
|
9
|
+
console.log(chalk.bold("\n📚 Initializing Point-Free Documentation\n"));
|
|
10
|
+
// Determine which libraries to download
|
|
11
|
+
let librariesToInit = LIBRARIES;
|
|
12
|
+
if (options.libs && !options.all) {
|
|
13
|
+
librariesToInit = [];
|
|
14
|
+
for (const name of options.libs) {
|
|
15
|
+
const lib = getLibrary(name);
|
|
16
|
+
if (lib) {
|
|
17
|
+
librariesToInit.push(lib);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log(chalk.yellow(` ⚠ Unknown library: ${name}`));
|
|
21
|
+
console.log(chalk.gray(` Available: ${LIBRARY_NAMES.join(", ")}`));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (librariesToInit.length === 0) {
|
|
26
|
+
console.log(chalk.red("No valid libraries specified."));
|
|
27
|
+
console.log(`\nUsage: pf-docs init --libs tca dependencies navigation`);
|
|
28
|
+
console.log(` pf-docs init --all`);
|
|
29
|
+
console.log(`\nAvailable libraries: ${LIBRARY_NAMES.join(", ")}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
console.log(chalk.blue(`Libraries to initialize: ${librariesToInit.map((l) => l.shortName).join(", ")}\n`));
|
|
33
|
+
// Clone repositories
|
|
34
|
+
console.log(chalk.bold("Cloning repositories..."));
|
|
35
|
+
for (const lib of librariesToInit) {
|
|
36
|
+
try {
|
|
37
|
+
await cloneLibrary(lib);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error(chalk.red(` ✗ Failed to clone ${lib.shortName}:`), error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Build search index
|
|
44
|
+
console.log(chalk.bold("\nBuilding search index..."));
|
|
45
|
+
openIndex();
|
|
46
|
+
let totalIndexed = 0;
|
|
47
|
+
for (const lib of librariesToInit) {
|
|
48
|
+
try {
|
|
49
|
+
const count = indexLibrary(lib);
|
|
50
|
+
console.log(` ✓ Indexed ${lib.shortName}: ${count} documents`);
|
|
51
|
+
totalIndexed += count;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.error(chalk.red(` ✗ Failed to index ${lib.shortName}:`), error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
closeIndex();
|
|
58
|
+
console.log(chalk.green(`\n✅ Done! Indexed ${totalIndexed} documents.\n`));
|
|
59
|
+
console.log(`Try it out:`);
|
|
60
|
+
console.log(chalk.gray(` pf-docs search "testing async effects"`));
|
|
61
|
+
console.log(chalk.gray(` pf-docs list tca`));
|
|
62
|
+
console.log(chalk.gray(` pf-docs get tca/Testing`));
|
|
63
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* list command - List available documentation articles
|
|
3
|
+
*/
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { listDocs, getStats, withIndex } from "../lib/index.js";
|
|
6
|
+
import { getLibrary, LIBRARIES, LIBRARY_NAMES } from "../config.js";
|
|
7
|
+
export function listCommand(lib, options) {
|
|
8
|
+
if (options.available) {
|
|
9
|
+
if (options.json) {
|
|
10
|
+
const libs = LIBRARIES.map(({ shortName, name, repo, description }) => ({
|
|
11
|
+
shortName, name, repo, description,
|
|
12
|
+
}));
|
|
13
|
+
console.log(JSON.stringify(libs, null, 2));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
console.log(chalk.bold(`\n📦 Available Libraries\n`));
|
|
17
|
+
for (const library of LIBRARIES) {
|
|
18
|
+
console.log(` ${chalk.blue(library.shortName.padEnd(24))} ${library.description}`);
|
|
19
|
+
console.log(chalk.gray(` ${"".padEnd(24)} ${library.repo}\n`));
|
|
20
|
+
}
|
|
21
|
+
console.log(chalk.gray(`Total: ${LIBRARIES.length} libraries`));
|
|
22
|
+
console.log(chalk.gray(`\nTo download: pf-docs init --libs <name> [<name>...]`));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const { docs, stats } = withIndex(() => ({
|
|
26
|
+
docs: listDocs(lib),
|
|
27
|
+
stats: getStats(),
|
|
28
|
+
}));
|
|
29
|
+
// JSON output for programmatic use
|
|
30
|
+
if (options.json) {
|
|
31
|
+
console.log(JSON.stringify({ docs, stats }, null, 2));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (docs.length === 0) {
|
|
35
|
+
if (lib) {
|
|
36
|
+
console.log(chalk.yellow(`\nNo documents found for library: ${lib}`));
|
|
37
|
+
console.log(chalk.gray(`Available libraries: ${LIBRARY_NAMES.join(", ")}`));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
console.log(chalk.yellow(`\nNo documents indexed yet.`));
|
|
41
|
+
console.log(chalk.gray(`Run 'pf-docs init --libs tca dependencies' to get started.`));
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
console.log(chalk.bold(`\n📄 Available Documentation\n`));
|
|
46
|
+
// Show stats summary
|
|
47
|
+
if (!lib) {
|
|
48
|
+
console.log(chalk.gray(`Indexed libraries:`));
|
|
49
|
+
for (const [libName, count] of Object.entries(stats.byLibrary)) {
|
|
50
|
+
const libConfig = getLibrary(libName);
|
|
51
|
+
const desc = libConfig?.description || "";
|
|
52
|
+
console.log(chalk.gray(` ${chalk.blue(libName)}: ${count} docs — ${desc}`));
|
|
53
|
+
}
|
|
54
|
+
console.log();
|
|
55
|
+
}
|
|
56
|
+
if (options.tree) {
|
|
57
|
+
// Group by library and show as tree
|
|
58
|
+
const byLibrary = new Map();
|
|
59
|
+
for (const doc of docs) {
|
|
60
|
+
if (!byLibrary.has(doc.library)) {
|
|
61
|
+
byLibrary.set(doc.library, []);
|
|
62
|
+
}
|
|
63
|
+
byLibrary.get(doc.library).push(doc);
|
|
64
|
+
}
|
|
65
|
+
for (const [library, libraryDocs] of byLibrary) {
|
|
66
|
+
console.log(chalk.blue(`${library}/`));
|
|
67
|
+
for (let i = 0; i < libraryDocs.length; i++) {
|
|
68
|
+
const doc = libraryDocs[i];
|
|
69
|
+
const relativePath = doc.path.replace(`${library}/`, "");
|
|
70
|
+
const isLast = i === libraryDocs.length - 1;
|
|
71
|
+
const prefix = isLast ? "└── " : "├── ";
|
|
72
|
+
console.log(chalk.gray(` ${prefix}${relativePath}`));
|
|
73
|
+
}
|
|
74
|
+
console.log();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Simple list
|
|
79
|
+
for (const doc of docs) {
|
|
80
|
+
console.log(chalk.blue(doc.path));
|
|
81
|
+
console.log(chalk.gray(` ${doc.title}`));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
console.log(chalk.gray(`\nTotal: ${docs.length} documents`));
|
|
85
|
+
console.log(chalk.gray(`\nTo view a document: pf-docs get <path>`));
|
|
86
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search command - Search across all indexed documentation
|
|
3
|
+
*/
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { search as searchIndex, withIndex } from "../lib/index.js";
|
|
6
|
+
export function searchCommand(query, options) {
|
|
7
|
+
const limit = options.limit ? parseInt(options.limit, 10) : 10;
|
|
8
|
+
const results = withIndex(() => searchIndex(query, { lib: options.lib, limit }));
|
|
9
|
+
if (options.json) {
|
|
10
|
+
console.log(JSON.stringify({ query, results }, null, 2));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (results.length === 0) {
|
|
14
|
+
console.log(chalk.yellow(`\nNo results found for: "${query}"`));
|
|
15
|
+
if (options.lib) {
|
|
16
|
+
console.log(chalk.gray(` (searched in library: ${options.lib})`));
|
|
17
|
+
}
|
|
18
|
+
console.log(chalk.gray(`\nTry a different query or run 'pf-docs list' to see available docs.`));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.log(chalk.bold(`\n🔍 Search results for: "${query}"\n`));
|
|
22
|
+
for (let i = 0; i < results.length; i++) {
|
|
23
|
+
const result = results[i];
|
|
24
|
+
console.log(chalk.blue(`${i + 1}. ${result.title}`));
|
|
25
|
+
console.log(chalk.gray(` Path: ${result.path}`));
|
|
26
|
+
console.log(chalk.gray(` ${result.snippet}`));
|
|
27
|
+
console.log();
|
|
28
|
+
}
|
|
29
|
+
console.log(chalk.gray(`\nTo view a document: pf-docs get <path>`));
|
|
30
|
+
console.log(chalk.gray(`Example: pf-docs get ${results[0].path}`));
|
|
31
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stats command - Show indexing statistics
|
|
3
|
+
*/
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { getStats, withIndex } from "../lib/index.js";
|
|
6
|
+
import { LIBRARIES, getLibrary } from "../config.js";
|
|
7
|
+
import { isLibraryCloned } from "../lib/repos.js";
|
|
8
|
+
export function statsCommand(options) {
|
|
9
|
+
const stats = withIndex(() => getStats());
|
|
10
|
+
if (options.json) {
|
|
11
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
console.log(chalk.bold("\n📊 pf-docs Statistics\n"));
|
|
15
|
+
console.log(chalk.blue(`Total indexed documents: ${stats.totalDocs}`));
|
|
16
|
+
console.log();
|
|
17
|
+
console.log(chalk.bold("Indexed libraries:"));
|
|
18
|
+
for (const [libName, count] of Object.entries(stats.byLibrary)) {
|
|
19
|
+
const libConfig = getLibrary(libName);
|
|
20
|
+
console.log(` ${chalk.green("●")} ${libName}: ${count} docs`);
|
|
21
|
+
if (libConfig) {
|
|
22
|
+
console.log(chalk.gray(` ${libConfig.description}`));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
console.log();
|
|
26
|
+
// Show available but not indexed libraries
|
|
27
|
+
const notIndexed = LIBRARIES.filter((lib) => !stats.byLibrary[lib.shortName] && !isLibraryCloned(lib));
|
|
28
|
+
if (notIndexed.length > 0) {
|
|
29
|
+
console.log(chalk.bold("Available libraries (not indexed):"));
|
|
30
|
+
for (const lib of notIndexed) {
|
|
31
|
+
console.log(` ${chalk.gray("○")} ${lib.shortName}`);
|
|
32
|
+
console.log(chalk.gray(` ${lib.description}`));
|
|
33
|
+
}
|
|
34
|
+
console.log();
|
|
35
|
+
console.log(chalk.gray(`To add: pf-docs init --libs ${notIndexed[0].shortName}`));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* update command - Update documentation from remote repositories
|
|
3
|
+
*/
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { LIBRARIES, getLibrary } from "../config.js";
|
|
6
|
+
import { updateLibrary, isLibraryCloned } from "../lib/repos.js";
|
|
7
|
+
import { indexLibrary, openIndex, closeIndex } from "../lib/index.js";
|
|
8
|
+
export async function updateCommand(options) {
|
|
9
|
+
console.log(chalk.bold("\n🔄 Updating Point-Free Documentation\n"));
|
|
10
|
+
// Determine which libraries to update
|
|
11
|
+
let librariesToUpdate = LIBRARIES.filter(isLibraryCloned);
|
|
12
|
+
if (options.libs) {
|
|
13
|
+
librariesToUpdate = [];
|
|
14
|
+
for (const name of options.libs) {
|
|
15
|
+
const lib = getLibrary(name);
|
|
16
|
+
if (lib && isLibraryCloned(lib)) {
|
|
17
|
+
librariesToUpdate.push(lib);
|
|
18
|
+
}
|
|
19
|
+
else if (lib) {
|
|
20
|
+
console.log(chalk.yellow(` ⚠ Library not initialized: ${name}. Run 'pf-docs init --libs ${name}' first.`));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
console.log(chalk.yellow(` ⚠ Unknown library: ${name}`));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (librariesToUpdate.length === 0) {
|
|
28
|
+
console.log(chalk.yellow("No libraries to update. Run 'pf-docs init' first."));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Update repositories
|
|
32
|
+
console.log(chalk.bold("Pulling latest changes..."));
|
|
33
|
+
const updatedLibs = [];
|
|
34
|
+
for (const lib of librariesToUpdate) {
|
|
35
|
+
const wasUpdated = await updateLibrary(lib);
|
|
36
|
+
if (wasUpdated) {
|
|
37
|
+
updatedLibs.push(lib);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Re-index updated libraries
|
|
41
|
+
if (updatedLibs.length > 0) {
|
|
42
|
+
console.log(chalk.bold("\nRe-indexing updated libraries..."));
|
|
43
|
+
openIndex();
|
|
44
|
+
for (const lib of updatedLibs) {
|
|
45
|
+
const count = indexLibrary(lib);
|
|
46
|
+
console.log(` ✓ Re-indexed ${lib.shortName}: ${count} documents`);
|
|
47
|
+
}
|
|
48
|
+
closeIndex();
|
|
49
|
+
}
|
|
50
|
+
console.log(chalk.green(`\n✅ Update complete!\n`));
|
|
51
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for Point-Free libraries
|
|
3
|
+
*/
|
|
4
|
+
export interface LibraryConfig {
|
|
5
|
+
name: string;
|
|
6
|
+
shortName: string;
|
|
7
|
+
repo: string;
|
|
8
|
+
docsPaths: string[];
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* All available Point-Free libraries
|
|
13
|
+
* Add or remove libraries here to customize what's indexed
|
|
14
|
+
*/
|
|
15
|
+
export declare const LIBRARIES: LibraryConfig[];
|
|
16
|
+
/**
|
|
17
|
+
* Get library config by short name
|
|
18
|
+
*/
|
|
19
|
+
export declare function getLibrary(shortName: string): LibraryConfig | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* All library short names
|
|
22
|
+
*/
|
|
23
|
+
export declare const LIBRARY_NAMES: string[];
|
|
24
|
+
export declare const PATHS: {
|
|
25
|
+
dataDir: string;
|
|
26
|
+
reposDir: string;
|
|
27
|
+
indexDb: string;
|
|
28
|
+
};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for Point-Free libraries
|
|
3
|
+
*/
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
/**
|
|
6
|
+
* All available Point-Free libraries
|
|
7
|
+
* Add or remove libraries here to customize what's indexed
|
|
8
|
+
*/
|
|
9
|
+
export const LIBRARIES = [
|
|
10
|
+
{
|
|
11
|
+
name: "swift-composable-architecture",
|
|
12
|
+
shortName: "tca",
|
|
13
|
+
repo: "pointfreeco/swift-composable-architecture",
|
|
14
|
+
docsPaths: ["Sources/ComposableArchitecture/Documentation.docc"],
|
|
15
|
+
description: "The Composable Architecture",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "swift-dependencies",
|
|
19
|
+
shortName: "dependencies",
|
|
20
|
+
repo: "pointfreeco/swift-dependencies",
|
|
21
|
+
docsPaths: ["Sources/Dependencies/Documentation.docc"],
|
|
22
|
+
description: "Dependency injection library",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "swift-navigation",
|
|
26
|
+
shortName: "navigation",
|
|
27
|
+
repo: "pointfreeco/swift-navigation",
|
|
28
|
+
docsPaths: [
|
|
29
|
+
"Sources/SwiftNavigation/Documentation.docc",
|
|
30
|
+
"Sources/SwiftUINavigation/Documentation.docc",
|
|
31
|
+
"Sources/UIKitNavigation/Documentation.docc",
|
|
32
|
+
"Sources/AppKitNavigation/Documentation.docc",
|
|
33
|
+
],
|
|
34
|
+
description: "Navigation tools for Swift",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "swift-perception",
|
|
38
|
+
shortName: "perception",
|
|
39
|
+
repo: "pointfreeco/swift-perception",
|
|
40
|
+
docsPaths: ["Sources/Perception/Documentation.docc"],
|
|
41
|
+
description: "@Observable backported to iOS 16",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "swift-sharing",
|
|
45
|
+
shortName: "sharing",
|
|
46
|
+
repo: "pointfreeco/swift-sharing",
|
|
47
|
+
docsPaths: ["Sources/Sharing/Documentation.docc"],
|
|
48
|
+
description: "Persistence & data sharing",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "swift-identified-collections",
|
|
52
|
+
shortName: "identified-collections",
|
|
53
|
+
repo: "pointfreeco/swift-identified-collections",
|
|
54
|
+
docsPaths: ["Sources/IdentifiedCollections/Documentation.docc"],
|
|
55
|
+
description: "Identifiable-aware collections",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "swift-case-paths",
|
|
59
|
+
shortName: "case-paths",
|
|
60
|
+
repo: "pointfreeco/swift-case-paths",
|
|
61
|
+
docsPaths: ["Sources/CasePaths/Documentation.docc"],
|
|
62
|
+
description: "Key paths for enum cases",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "swift-custom-dump",
|
|
66
|
+
shortName: "custom-dump",
|
|
67
|
+
repo: "pointfreeco/swift-custom-dump",
|
|
68
|
+
docsPaths: ["Sources/CustomDump/Documentation.docc"],
|
|
69
|
+
description: "Debugging/diffing tools",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "swift-concurrency-extras",
|
|
73
|
+
shortName: "concurrency-extras",
|
|
74
|
+
repo: "pointfreeco/swift-concurrency-extras",
|
|
75
|
+
docsPaths: ["Sources/ConcurrencyExtras/Documentation.docc"],
|
|
76
|
+
description: "Testable async/await",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "swift-clocks",
|
|
80
|
+
shortName: "clocks",
|
|
81
|
+
repo: "pointfreeco/swift-clocks",
|
|
82
|
+
docsPaths: ["Sources/Clocks/Documentation.docc"],
|
|
83
|
+
description: "Testable Swift concurrency clocks",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "swift-snapshot-testing",
|
|
87
|
+
shortName: "snapshot-testing",
|
|
88
|
+
repo: "pointfreeco/swift-snapshot-testing",
|
|
89
|
+
docsPaths: [
|
|
90
|
+
"Sources/SnapshotTesting/Documentation.docc",
|
|
91
|
+
"Sources/InlineSnapshotTesting/Documentation.docc",
|
|
92
|
+
],
|
|
93
|
+
description: "Snapshot testing library",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "swift-issue-reporting",
|
|
97
|
+
shortName: "issue-reporting",
|
|
98
|
+
repo: "pointfreeco/swift-issue-reporting",
|
|
99
|
+
docsPaths: ["Sources/IssueReporting/Documentation.docc"],
|
|
100
|
+
description: "Runtime warnings & assertions",
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
/**
|
|
104
|
+
* Get library config by short name
|
|
105
|
+
*/
|
|
106
|
+
export function getLibrary(shortName) {
|
|
107
|
+
return LIBRARIES.find((lib) => lib.shortName === shortName || lib.name === shortName);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* All library short names
|
|
111
|
+
*/
|
|
112
|
+
export const LIBRARY_NAMES = LIBRARIES.map((lib) => lib.shortName);
|
|
113
|
+
/**
|
|
114
|
+
* Paths configuration
|
|
115
|
+
*/
|
|
116
|
+
const dataDir = new URL("../data", import.meta.url).pathname;
|
|
117
|
+
export const PATHS = {
|
|
118
|
+
dataDir,
|
|
119
|
+
reposDir: join(dataDir, "repos"),
|
|
120
|
+
indexDb: join(dataDir, "index.db"),
|
|
121
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search index using SQLite FTS5 for full-text search
|
|
3
|
+
*/
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
import { LibraryConfig } from "../config.js";
|
|
6
|
+
export interface DocEntry {
|
|
7
|
+
library: string;
|
|
8
|
+
path: string;
|
|
9
|
+
title: string;
|
|
10
|
+
}
|
|
11
|
+
export interface DocWithContent extends DocEntry {
|
|
12
|
+
content: string;
|
|
13
|
+
}
|
|
14
|
+
export interface IndexStats {
|
|
15
|
+
totalDocs: number;
|
|
16
|
+
byLibrary: Record<string, number>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Initialize or open the search index database
|
|
20
|
+
*/
|
|
21
|
+
export declare function openIndex(): Database.Database;
|
|
22
|
+
/**
|
|
23
|
+
* Index all documentation files for a library
|
|
24
|
+
*/
|
|
25
|
+
export declare function indexLibrary(lib: LibraryConfig): number;
|
|
26
|
+
/**
|
|
27
|
+
* Search the index
|
|
28
|
+
*/
|
|
29
|
+
export interface SearchResult {
|
|
30
|
+
library: string;
|
|
31
|
+
path: string;
|
|
32
|
+
title: string;
|
|
33
|
+
snippet: string;
|
|
34
|
+
score: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function search(query: string, options?: {
|
|
37
|
+
lib?: string;
|
|
38
|
+
limit?: number;
|
|
39
|
+
}): SearchResult[];
|
|
40
|
+
/**
|
|
41
|
+
* List all indexed documents
|
|
42
|
+
*/
|
|
43
|
+
export declare function listDocs(lib?: string): DocEntry[];
|
|
44
|
+
/**
|
|
45
|
+
* Get a specific document by path
|
|
46
|
+
*/
|
|
47
|
+
export declare function getDoc(path: string): DocWithContent | null;
|
|
48
|
+
/**
|
|
49
|
+
* Get index statistics
|
|
50
|
+
*/
|
|
51
|
+
export declare function getStats(): IndexStats;
|
|
52
|
+
/**
|
|
53
|
+
* Run a function with the index open, closing it automatically afterward.
|
|
54
|
+
*/
|
|
55
|
+
export declare function withIndex<T>(fn: () => T): T;
|
|
56
|
+
/**
|
|
57
|
+
* Close the database connection
|
|
58
|
+
*/
|
|
59
|
+
export declare function closeIndex(): void;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search index using SQLite FTS5 for full-text search
|
|
3
|
+
*/
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
6
|
+
import { glob } from "glob";
|
|
7
|
+
import { basename, join } from "path";
|
|
8
|
+
import { PATHS } from "../config.js";
|
|
9
|
+
import { getDocsPaths } from "./repos.js";
|
|
10
|
+
import { cleanMarkdown, extractTitle } from "./markdown.js";
|
|
11
|
+
let db = null;
|
|
12
|
+
/**
|
|
13
|
+
* Initialize or open the search index database
|
|
14
|
+
*/
|
|
15
|
+
export function openIndex() {
|
|
16
|
+
if (db)
|
|
17
|
+
return db;
|
|
18
|
+
// Ensure data directory exists
|
|
19
|
+
if (!existsSync(PATHS.dataDir)) {
|
|
20
|
+
mkdirSync(PATHS.dataDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
db = new Database(PATHS.indexDb);
|
|
23
|
+
// Create tables if they don't exist
|
|
24
|
+
db.exec(`
|
|
25
|
+
CREATE TABLE IF NOT EXISTS docs (
|
|
26
|
+
id INTEGER PRIMARY KEY,
|
|
27
|
+
library TEXT NOT NULL,
|
|
28
|
+
path TEXT NOT NULL,
|
|
29
|
+
title TEXT,
|
|
30
|
+
content TEXT,
|
|
31
|
+
UNIQUE(library, path)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS docs_fts USING fts5(
|
|
35
|
+
title,
|
|
36
|
+
content,
|
|
37
|
+
library,
|
|
38
|
+
path,
|
|
39
|
+
content='docs',
|
|
40
|
+
content_rowid='id'
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
-- Triggers to keep FTS index in sync
|
|
44
|
+
CREATE TRIGGER IF NOT EXISTS docs_ai AFTER INSERT ON docs BEGIN
|
|
45
|
+
INSERT INTO docs_fts(rowid, title, content, library, path)
|
|
46
|
+
VALUES (new.id, new.title, new.content, new.library, new.path);
|
|
47
|
+
END;
|
|
48
|
+
|
|
49
|
+
CREATE TRIGGER IF NOT EXISTS docs_ad AFTER DELETE ON docs BEGIN
|
|
50
|
+
INSERT INTO docs_fts(docs_fts, rowid, title, content, library, path)
|
|
51
|
+
VALUES ('delete', old.id, old.title, old.content, old.library, old.path);
|
|
52
|
+
END;
|
|
53
|
+
|
|
54
|
+
CREATE TRIGGER IF NOT EXISTS docs_au AFTER UPDATE ON docs BEGIN
|
|
55
|
+
INSERT INTO docs_fts(docs_fts, rowid, title, content, library, path)
|
|
56
|
+
VALUES ('delete', old.id, old.title, old.content, old.library, old.path);
|
|
57
|
+
INSERT INTO docs_fts(rowid, title, content, library, path)
|
|
58
|
+
VALUES (new.id, new.title, new.content, new.library, new.path);
|
|
59
|
+
END;
|
|
60
|
+
`);
|
|
61
|
+
return db;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Index all documentation files for a library
|
|
65
|
+
*/
|
|
66
|
+
export function indexLibrary(lib) {
|
|
67
|
+
const db = openIndex();
|
|
68
|
+
const docsPaths = getDocsPaths(lib);
|
|
69
|
+
const insertStmt = db.prepare(`
|
|
70
|
+
INSERT OR REPLACE INTO docs (library, path, title, content)
|
|
71
|
+
VALUES (?, ?, ?, ?)
|
|
72
|
+
`);
|
|
73
|
+
let indexed = 0;
|
|
74
|
+
for (const docsPath of docsPaths) {
|
|
75
|
+
if (!existsSync(docsPath)) {
|
|
76
|
+
// Skip if this docs path doesn't exist (some may be optional)
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// Find all markdown files
|
|
80
|
+
const mdFiles = glob.sync("**/*.md", { cwd: docsPath });
|
|
81
|
+
for (const file of mdFiles) {
|
|
82
|
+
const fullPath = join(docsPath, file);
|
|
83
|
+
try {
|
|
84
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
85
|
+
const cleanedContent = cleanMarkdown(content);
|
|
86
|
+
const title = extractTitle(content) || basename(file, ".md");
|
|
87
|
+
// Logical path like "tca/Testing" or "tca/Articles/Performance"
|
|
88
|
+
const docPath = `${lib.shortName}/${file.replace(/\.md$/, "")}`;
|
|
89
|
+
insertStmt.run(lib.shortName, docPath, title, cleanedContent);
|
|
90
|
+
indexed++;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error(` Warning: Failed to index ${fullPath}:`, error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return indexed;
|
|
98
|
+
}
|
|
99
|
+
export function search(query, options = {}) {
|
|
100
|
+
const db = openIndex();
|
|
101
|
+
const limit = options.limit || 10;
|
|
102
|
+
// Escape special FTS5 characters and format query
|
|
103
|
+
const ftsQuery = query
|
|
104
|
+
.replace(/['"]/g, "") // Remove quotes
|
|
105
|
+
.split(/\s+/)
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.map((term) => `"${term}"*`) // Prefix matching
|
|
108
|
+
.join(" ");
|
|
109
|
+
if (!ftsQuery) {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
let sql = `
|
|
113
|
+
SELECT
|
|
114
|
+
library,
|
|
115
|
+
path,
|
|
116
|
+
title,
|
|
117
|
+
snippet(docs_fts, 1, '**', '**', '...', 32) as snippet,
|
|
118
|
+
bm25(docs_fts) as score
|
|
119
|
+
FROM docs_fts
|
|
120
|
+
WHERE docs_fts MATCH ?
|
|
121
|
+
`;
|
|
122
|
+
const params = [ftsQuery];
|
|
123
|
+
if (options.lib) {
|
|
124
|
+
sql += ` AND library = ?`;
|
|
125
|
+
params.push(options.lib);
|
|
126
|
+
}
|
|
127
|
+
sql += ` ORDER BY score LIMIT ?`;
|
|
128
|
+
params.push(limit);
|
|
129
|
+
try {
|
|
130
|
+
const stmt = db.prepare(sql);
|
|
131
|
+
return stmt.all(...params);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
// If FTS query fails, try simpler LIKE search
|
|
135
|
+
console.error("FTS search failed, falling back to LIKE search:", error);
|
|
136
|
+
return fallbackSearch(query, options);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Fallback search using LIKE (when FTS fails)
|
|
141
|
+
*/
|
|
142
|
+
function fallbackSearch(query, options = {}) {
|
|
143
|
+
const db = openIndex();
|
|
144
|
+
const limit = options.limit || 10;
|
|
145
|
+
let sql = `
|
|
146
|
+
SELECT
|
|
147
|
+
library,
|
|
148
|
+
path,
|
|
149
|
+
title,
|
|
150
|
+
substr(content, 1, 200) as snippet,
|
|
151
|
+
0 as score
|
|
152
|
+
FROM docs
|
|
153
|
+
WHERE (title LIKE ? OR content LIKE ?)
|
|
154
|
+
`;
|
|
155
|
+
const likePattern = `%${query}%`;
|
|
156
|
+
const params = [likePattern, likePattern];
|
|
157
|
+
if (options.lib) {
|
|
158
|
+
sql += ` AND library = ?`;
|
|
159
|
+
params.push(options.lib);
|
|
160
|
+
}
|
|
161
|
+
sql += ` LIMIT ?`;
|
|
162
|
+
params.push(limit);
|
|
163
|
+
try {
|
|
164
|
+
const stmt = db.prepare(sql);
|
|
165
|
+
return stmt.all(...params);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* List all indexed documents
|
|
173
|
+
*/
|
|
174
|
+
export function listDocs(lib) {
|
|
175
|
+
const db = openIndex();
|
|
176
|
+
let sql = `SELECT library, path, title FROM docs`;
|
|
177
|
+
const params = [];
|
|
178
|
+
if (lib) {
|
|
179
|
+
sql += ` WHERE library = ?`;
|
|
180
|
+
params.push(lib);
|
|
181
|
+
}
|
|
182
|
+
sql += ` ORDER BY library, path`;
|
|
183
|
+
const stmt = db.prepare(sql);
|
|
184
|
+
return stmt.all(...params);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get a specific document by path
|
|
188
|
+
*/
|
|
189
|
+
export function getDoc(path) {
|
|
190
|
+
const db = openIndex();
|
|
191
|
+
const stmt = db.prepare(`SELECT library, path, title, content FROM docs WHERE path = ?`);
|
|
192
|
+
return stmt.get(path);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get index statistics
|
|
196
|
+
*/
|
|
197
|
+
export function getStats() {
|
|
198
|
+
const db = openIndex();
|
|
199
|
+
const totalStmt = db.prepare(`SELECT COUNT(*) as count FROM docs`);
|
|
200
|
+
const total = totalStmt.get().count;
|
|
201
|
+
const byLibStmt = db.prepare(`SELECT library, COUNT(*) as count FROM docs GROUP BY library`);
|
|
202
|
+
const byLibRows = byLibStmt.all();
|
|
203
|
+
const byLibrary = Object.fromEntries(byLibRows.map((row) => [row.library, row.count]));
|
|
204
|
+
return { totalDocs: total, byLibrary };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Run a function with the index open, closing it automatically afterward.
|
|
208
|
+
*/
|
|
209
|
+
export function withIndex(fn) {
|
|
210
|
+
openIndex();
|
|
211
|
+
try {
|
|
212
|
+
return fn();
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
closeIndex();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Close the database connection
|
|
220
|
+
*/
|
|
221
|
+
export function closeIndex() {
|
|
222
|
+
if (db) {
|
|
223
|
+
db.close();
|
|
224
|
+
db = null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown processing utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles DocC-flavored markdown and converts it to clean, AI-friendly markdown.
|
|
5
|
+
*/
|
|
6
|
+
import type { DocWithContent } from "./index.js";
|
|
7
|
+
/**
|
|
8
|
+
* Extract the title from a markdown document
|
|
9
|
+
* Handles both standard markdown headers and DocC frontmatter
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractTitle(content: string): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Clean DocC-flavored markdown for better search and AI consumption
|
|
14
|
+
*/
|
|
15
|
+
export declare function cleanMarkdown(content: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Format a document for output to the terminal/AI
|
|
18
|
+
*/
|
|
19
|
+
export declare function formatDocForOutput(doc: DocWithContent): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown processing utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles DocC-flavored markdown and converts it to clean, AI-friendly markdown.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Extract the title from a markdown document
|
|
8
|
+
* Handles both standard markdown headers and DocC frontmatter
|
|
9
|
+
*/
|
|
10
|
+
export function extractTitle(content) {
|
|
11
|
+
// Try to find a DocC title directive: # ``SymbolName``
|
|
12
|
+
const doccTitleMatch = content.match(/^#\s+``([^`]+)``/m);
|
|
13
|
+
if (doccTitleMatch) {
|
|
14
|
+
return doccTitleMatch[1];
|
|
15
|
+
}
|
|
16
|
+
// Try standard markdown h1
|
|
17
|
+
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
18
|
+
if (h1Match) {
|
|
19
|
+
return h1Match[1].trim();
|
|
20
|
+
}
|
|
21
|
+
// Try YAML frontmatter title
|
|
22
|
+
const yamlMatch = content.match(/^---\s*\n.*?title:\s*(.+?)\n.*?---/s);
|
|
23
|
+
if (yamlMatch) {
|
|
24
|
+
return yamlMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Clean DocC-flavored markdown for better search and AI consumption
|
|
30
|
+
*/
|
|
31
|
+
export function cleanMarkdown(content) {
|
|
32
|
+
let cleaned = content;
|
|
33
|
+
// Remove DocC directives like @Metadata, @Options, etc.
|
|
34
|
+
cleaned = cleaned.replace(/@\w+\s*\{[^}]*\}/g, "");
|
|
35
|
+
// Convert DocC symbol references ``Symbol`` to just Symbol
|
|
36
|
+
cleaned = cleaned.replace(/``([^`]+)``/g, "$1");
|
|
37
|
+
// Convert DocC links like <doc:ArticleName> to [ArticleName]
|
|
38
|
+
cleaned = cleaned.replace(/<doc:([^>]+)>/g, "[$1]");
|
|
39
|
+
// Remove @Row, @Column and other DocC layout directives
|
|
40
|
+
cleaned = cleaned.replace(/@(Row|Column|Image|Video|Links|TabNavigator|Tab)\s*(\{[^}]*\})?/g, "");
|
|
41
|
+
// Clean up excessive whitespace
|
|
42
|
+
cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
|
|
43
|
+
return cleaned.trim();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Format a document for output to the terminal/AI
|
|
47
|
+
*/
|
|
48
|
+
export function formatDocForOutput(doc) {
|
|
49
|
+
const header = `# ${doc.title}
|
|
50
|
+
|
|
51
|
+
> Library: ${doc.library}
|
|
52
|
+
> Path: ${doc.path}
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
`;
|
|
57
|
+
return header + doc.content;
|
|
58
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git operations for cloning and updating Point-Free repositories
|
|
3
|
+
*/
|
|
4
|
+
import { LibraryConfig } from "../config.js";
|
|
5
|
+
/**
|
|
6
|
+
* Clone a library repository (sparse checkout, docs only)
|
|
7
|
+
*/
|
|
8
|
+
export declare function cloneLibrary(lib: LibraryConfig): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Update a library repository
|
|
11
|
+
*/
|
|
12
|
+
export declare function updateLibrary(lib: LibraryConfig): Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Get all local paths to a library's docs
|
|
15
|
+
*/
|
|
16
|
+
export declare function getDocsPaths(lib: LibraryConfig): string[];
|
|
17
|
+
/**
|
|
18
|
+
* Check if a library is cloned
|
|
19
|
+
*/
|
|
20
|
+
export declare function isLibraryCloned(lib: LibraryConfig): boolean;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git operations for cloning and updating Point-Free repositories
|
|
3
|
+
*/
|
|
4
|
+
import { simpleGit } from "simple-git";
|
|
5
|
+
import { existsSync, mkdirSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { PATHS } from "../config.js";
|
|
8
|
+
/**
|
|
9
|
+
* Clone a library repository (sparse checkout, docs only)
|
|
10
|
+
*/
|
|
11
|
+
export async function cloneLibrary(lib) {
|
|
12
|
+
const repoDir = join(PATHS.reposDir, lib.name);
|
|
13
|
+
// Ensure repos directory exists
|
|
14
|
+
if (!existsSync(PATHS.reposDir)) {
|
|
15
|
+
mkdirSync(PATHS.reposDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
if (existsSync(repoDir)) {
|
|
18
|
+
console.log(` Repository already exists: ${lib.name}`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.log(` Cloning ${lib.repo}...`);
|
|
22
|
+
const git = simpleGit();
|
|
23
|
+
// Sparse checkout: only download docs directories
|
|
24
|
+
await git.clone(`https://github.com/${lib.repo}.git`, repoDir, [
|
|
25
|
+
"--depth",
|
|
26
|
+
"1",
|
|
27
|
+
"--filter=blob:none",
|
|
28
|
+
"--sparse",
|
|
29
|
+
]);
|
|
30
|
+
const repoGit = simpleGit(repoDir);
|
|
31
|
+
// Set up sparse checkout to only get the docs folders
|
|
32
|
+
await repoGit.raw(["sparse-checkout", "init", "--cone"]);
|
|
33
|
+
// Set all docs paths for this library
|
|
34
|
+
await repoGit.raw(["sparse-checkout", "set", ...lib.docsPaths]);
|
|
35
|
+
console.log(` ✓ Cloned ${lib.shortName}`);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Update a library repository
|
|
39
|
+
*/
|
|
40
|
+
export async function updateLibrary(lib) {
|
|
41
|
+
const repoDir = join(PATHS.reposDir, lib.name);
|
|
42
|
+
if (!existsSync(repoDir)) {
|
|
43
|
+
console.log(` Repository not found: ${lib.name}. Run 'pf-docs init' first.`);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
console.log(` Updating ${lib.shortName}...`);
|
|
47
|
+
const git = simpleGit(repoDir);
|
|
48
|
+
try {
|
|
49
|
+
const pullResult = await git.pull();
|
|
50
|
+
if (pullResult.summary.changes > 0) {
|
|
51
|
+
console.log(` ✓ Updated ${lib.shortName} (${pullResult.summary.changes} changes)`);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
console.log(` ✓ ${lib.shortName} is up to date`);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
console.error(` ✗ Failed to update ${lib.shortName}:`, error);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get all local paths to a library's docs
|
|
66
|
+
*/
|
|
67
|
+
export function getDocsPaths(lib) {
|
|
68
|
+
return lib.docsPaths.map((docsPath) => join(PATHS.reposDir, lib.name, docsPath));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check if a library is cloned
|
|
72
|
+
*/
|
|
73
|
+
export function isLibraryCloned(lib) {
|
|
74
|
+
const repoDir = join(PATHS.reposDir, lib.name);
|
|
75
|
+
return existsSync(repoDir);
|
|
76
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pointfree-docs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool for searching Point-Free library documentation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pf-docs": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "tsc --watch",
|
|
16
|
+
"start": "node dist/cli.js",
|
|
17
|
+
"link": "npm run build && npm link",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/ronnie3786/pointfree-docs.git"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"pointfree",
|
|
26
|
+
"point-free",
|
|
27
|
+
"swift",
|
|
28
|
+
"documentation",
|
|
29
|
+
"cli",
|
|
30
|
+
"composable-architecture",
|
|
31
|
+
"tca"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"better-sqlite3": "^11.0.0",
|
|
36
|
+
"commander": "^12.0.0",
|
|
37
|
+
"glob": "^10.3.0",
|
|
38
|
+
"simple-git": "^3.22.0",
|
|
39
|
+
"chalk": "^5.3.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/better-sqlite3": "^7.6.9",
|
|
43
|
+
"@types/node": "^20.11.0",
|
|
44
|
+
"typescript": "^5.3.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|