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 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
@@ -0,0 +1,7 @@
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
+ export {};
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,9 @@
1
+ /**
2
+ * get command - Retrieve a specific documentation article
3
+ */
4
+ interface GetOptions {
5
+ json?: boolean;
6
+ raw?: boolean;
7
+ }
8
+ export declare function getCommand(path: string, options?: GetOptions): void;
9
+ export {};
@@ -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,9 @@
1
+ /**
2
+ * init command - Download and index Point-Free documentation
3
+ */
4
+ interface InitOptions {
5
+ libs?: string[];
6
+ all?: boolean;
7
+ }
8
+ export declare function initCommand(options: InitOptions): Promise<void>;
9
+ export {};
@@ -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,10 @@
1
+ /**
2
+ * list command - List available documentation articles
3
+ */
4
+ interface ListOptions {
5
+ tree?: boolean;
6
+ json?: boolean;
7
+ available?: boolean;
8
+ }
9
+ export declare function listCommand(lib: string | undefined, options: ListOptions): void;
10
+ export {};
@@ -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,10 @@
1
+ /**
2
+ * search command - Search across all indexed documentation
3
+ */
4
+ interface SearchOptions {
5
+ lib?: string;
6
+ limit?: string;
7
+ json?: boolean;
8
+ }
9
+ export declare function searchCommand(query: string, options: SearchOptions): void;
10
+ export {};
@@ -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,8 @@
1
+ /**
2
+ * stats command - Show indexing statistics
3
+ */
4
+ interface StatsOptions {
5
+ json?: boolean;
6
+ }
7
+ export declare function statsCommand(options: StatsOptions): void;
8
+ export {};
@@ -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,8 @@
1
+ /**
2
+ * update command - Update documentation from remote repositories
3
+ */
4
+ interface UpdateOptions {
5
+ libs?: string[];
6
+ }
7
+ export declare function updateCommand(options: UpdateOptions): Promise<void>;
8
+ export {};
@@ -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
+ }
@@ -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
+ }