pointfree-docs 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib/index.js CHANGED
@@ -5,8 +5,8 @@ import Database from "better-sqlite3";
5
5
  import { existsSync, mkdirSync, readFileSync } from "fs";
6
6
  import { glob } from "glob";
7
7
  import { basename, join } from "path";
8
- import { PATHS } from "../config.js";
9
- import { getDocsPaths } from "./repos.js";
8
+ import { PATHS, EXAMPLES_CONFIG, EPISODES_CONFIG } from "../config.js";
9
+ import { getDocsPaths, getExamplesPaths, getEpisodesPath } from "./repos.js";
10
10
  import { cleanMarkdown, extractTitle } from "./markdown.js";
11
11
  let db = null;
12
12
  /**
@@ -28,6 +28,7 @@ export function openIndex() {
28
28
  path TEXT NOT NULL,
29
29
  title TEXT,
30
30
  content TEXT,
31
+ source TEXT NOT NULL DEFAULT 'docs',
31
32
  UNIQUE(library, path)
32
33
  );
33
34
 
@@ -36,26 +37,40 @@ export function openIndex() {
36
37
  content,
37
38
  library,
38
39
  path,
40
+ source,
39
41
  content='docs',
40
42
  content_rowid='id'
41
43
  );
44
+ `);
45
+ // Add source column if it doesn't exist (migration for existing databases)
46
+ try {
47
+ db.exec(`ALTER TABLE docs ADD COLUMN source TEXT NOT NULL DEFAULT 'docs'`);
48
+ }
49
+ catch {
50
+ // Column already exists, ignore
51
+ }
52
+ // Drop and recreate triggers to ensure they include the source column
53
+ // This handles migration from older schema versions
54
+ db.exec(`
55
+ DROP TRIGGER IF EXISTS docs_ai;
56
+ DROP TRIGGER IF EXISTS docs_ad;
57
+ DROP TRIGGER IF EXISTS docs_au;
42
58
 
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);
59
+ CREATE TRIGGER docs_ai AFTER INSERT ON docs BEGIN
60
+ INSERT INTO docs_fts(rowid, title, content, library, path, source)
61
+ VALUES (new.id, new.title, new.content, new.library, new.path, new.source);
47
62
  END;
48
63
 
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);
64
+ CREATE TRIGGER docs_ad AFTER DELETE ON docs BEGIN
65
+ INSERT INTO docs_fts(docs_fts, rowid, title, content, library, path, source)
66
+ VALUES ('delete', old.id, old.title, old.content, old.library, old.path, old.source);
52
67
  END;
53
68
 
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);
69
+ CREATE TRIGGER docs_au AFTER UPDATE ON docs BEGIN
70
+ INSERT INTO docs_fts(docs_fts, rowid, title, content, library, path, source)
71
+ VALUES ('delete', old.id, old.title, old.content, old.library, old.path, old.source);
72
+ INSERT INTO docs_fts(rowid, title, content, library, path, source)
73
+ VALUES (new.id, new.title, new.content, new.library, new.path, new.source);
59
74
  END;
60
75
  `);
61
76
  return db;
@@ -67,8 +82,8 @@ export function indexLibrary(lib) {
67
82
  const db = openIndex();
68
83
  const docsPaths = getDocsPaths(lib);
69
84
  const insertStmt = db.prepare(`
70
- INSERT OR REPLACE INTO docs (library, path, title, content)
71
- VALUES (?, ?, ?, ?)
85
+ INSERT OR REPLACE INTO docs (library, path, title, content, source)
86
+ VALUES (?, ?, ?, ?, ?)
72
87
  `);
73
88
  let indexed = 0;
74
89
  for (const docsPath of docsPaths) {
@@ -86,7 +101,7 @@ export function indexLibrary(lib) {
86
101
  const title = extractTitle(content) || basename(file, ".md");
87
102
  // Logical path like "tca/Testing" or "tca/Articles/Performance"
88
103
  const docPath = `${lib.shortName}/${file.replace(/\.md$/, "")}`;
89
- insertStmt.run(lib.shortName, docPath, title, cleanedContent);
104
+ insertStmt.run(lib.shortName, docPath, title, cleanedContent, "docs");
90
105
  indexed++;
91
106
  }
92
107
  catch (error) {
@@ -96,9 +111,112 @@ export function indexLibrary(lib) {
96
111
  }
97
112
  return indexed;
98
113
  }
114
+ /**
115
+ * Index TCA examples (CaseStudies, etc.)
116
+ */
117
+ export function indexExamples() {
118
+ const db = openIndex();
119
+ const examplesPaths = getExamplesPaths();
120
+ const insertStmt = db.prepare(`
121
+ INSERT OR REPLACE INTO docs (library, path, title, content, source)
122
+ VALUES (?, ?, ?, ?, ?)
123
+ `);
124
+ let indexed = 0;
125
+ for (const examplesPath of examplesPaths) {
126
+ if (!existsSync(examplesPath)) {
127
+ continue;
128
+ }
129
+ // Find all Swift files
130
+ for (const pattern of EXAMPLES_CONFIG.filePatterns) {
131
+ const files = glob.sync(pattern, { cwd: examplesPath });
132
+ for (const file of files) {
133
+ const fullPath = join(examplesPath, file);
134
+ try {
135
+ const content = readFileSync(fullPath, "utf-8");
136
+ // Extract title from filename or first struct/class
137
+ const title = extractSwiftTitle(content) || basename(file, ".swift");
138
+ // Get the example category from the path (e.g., "CaseStudies", "SyncUps")
139
+ const category = basename(examplesPath);
140
+ const docPath = `examples/${category}/${file}`;
141
+ insertStmt.run("examples", docPath, title, content, "examples");
142
+ indexed++;
143
+ }
144
+ catch (error) {
145
+ console.error(` Warning: Failed to index ${fullPath}:`, error);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ return indexed;
151
+ }
152
+ /**
153
+ * Index episode code samples
154
+ */
155
+ export function indexEpisodes() {
156
+ const db = openIndex();
157
+ const episodesPath = getEpisodesPath();
158
+ if (!existsSync(episodesPath)) {
159
+ return 0;
160
+ }
161
+ const insertStmt = db.prepare(`
162
+ INSERT OR REPLACE INTO docs (library, path, title, content, source)
163
+ VALUES (?, ?, ?, ?, ?)
164
+ `);
165
+ let indexed = 0;
166
+ // Find all episode directories (0001-xxx, 0002-xxx, etc.)
167
+ const episodeDirs = glob.sync("[0-9][0-9][0-9][0-9]-*", { cwd: episodesPath });
168
+ for (const episodeDir of episodeDirs) {
169
+ const episodePath = join(episodesPath, episodeDir);
170
+ // Find all Swift files in this episode
171
+ for (const pattern of EPISODES_CONFIG.filePatterns) {
172
+ const files = glob.sync(pattern, { cwd: episodePath });
173
+ for (const file of files) {
174
+ const fullPath = join(episodePath, file);
175
+ try {
176
+ const content = readFileSync(fullPath, "utf-8");
177
+ // Title: episode name + file name
178
+ const episodeName = formatEpisodeName(episodeDir);
179
+ const fileName = basename(file, ".swift");
180
+ const title = `${episodeName}: ${fileName}`;
181
+ const docPath = `episodes/${episodeDir}/${file}`;
182
+ insertStmt.run("episodes", docPath, title, content, "episodes");
183
+ indexed++;
184
+ }
185
+ catch (error) {
186
+ console.error(` Warning: Failed to index ${fullPath}:`, error);
187
+ }
188
+ }
189
+ }
190
+ }
191
+ return indexed;
192
+ }
193
+ /**
194
+ * Extract title from Swift file (first struct/class/enum name)
195
+ */
196
+ function extractSwiftTitle(content) {
197
+ // Match struct, class, enum, or actor declarations
198
+ const match = content.match(/(?:struct|class|enum|actor)\s+(\w+)/);
199
+ return match ? match[1] : null;
200
+ }
201
+ /**
202
+ * Format episode directory name to readable title
203
+ * "0156-testable-state-pt1" -> "Episode 156: Testable State Pt1"
204
+ */
205
+ function formatEpisodeName(dirName) {
206
+ const match = dirName.match(/^(\d+)-(.+)$/);
207
+ if (!match)
208
+ return dirName;
209
+ const num = parseInt(match[1], 10);
210
+ const name = match[2]
211
+ .split("-")
212
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
213
+ .join(" ");
214
+ return `Ep${num}: ${name}`;
215
+ }
99
216
  export function search(query, options = {}) {
100
217
  const db = openIndex();
101
218
  const limit = options.limit || 10;
219
+ const source = options.source || "docs"; // Default to docs only
102
220
  // Escape special FTS5 characters and format query
103
221
  const ftsQuery = query
104
222
  .replace(/['"]/g, "") // Remove quotes
@@ -115,7 +233,8 @@ export function search(query, options = {}) {
115
233
  path,
116
234
  title,
117
235
  snippet(docs_fts, 1, '**', '**', '...', 32) as snippet,
118
- bm25(docs_fts) as score
236
+ bm25(docs_fts) as score,
237
+ source
119
238
  FROM docs_fts
120
239
  WHERE docs_fts MATCH ?
121
240
  `;
@@ -124,6 +243,11 @@ export function search(query, options = {}) {
124
243
  sql += ` AND library = ?`;
125
244
  params.push(options.lib);
126
245
  }
246
+ // Filter by source unless "all" is specified
247
+ if (source !== "all") {
248
+ sql += ` AND source = ?`;
249
+ params.push(source);
250
+ }
127
251
  sql += ` ORDER BY score LIMIT ?`;
128
252
  params.push(limit);
129
253
  try {
@@ -142,13 +266,15 @@ export function search(query, options = {}) {
142
266
  function fallbackSearch(query, options = {}) {
143
267
  const db = openIndex();
144
268
  const limit = options.limit || 10;
269
+ const source = options.source || "docs";
145
270
  let sql = `
146
271
  SELECT
147
272
  library,
148
273
  path,
149
274
  title,
150
275
  substr(content, 1, 200) as snippet,
151
- 0 as score
276
+ 0 as score,
277
+ source
152
278
  FROM docs
153
279
  WHERE (title LIKE ? OR content LIKE ?)
154
280
  `;
@@ -158,6 +284,10 @@ function fallbackSearch(query, options = {}) {
158
284
  sql += ` AND library = ?`;
159
285
  params.push(options.lib);
160
286
  }
287
+ if (source !== "all") {
288
+ sql += ` AND source = ?`;
289
+ params.push(source);
290
+ }
161
291
  sql += ` LIMIT ?`;
162
292
  params.push(limit);
163
293
  try {
@@ -171,15 +301,19 @@ function fallbackSearch(query, options = {}) {
171
301
  /**
172
302
  * List all indexed documents
173
303
  */
174
- export function listDocs(lib) {
304
+ export function listDocs(lib, source) {
175
305
  const db = openIndex();
176
- let sql = `SELECT library, path, title FROM docs`;
306
+ let sql = `SELECT library, path, title, source FROM docs WHERE 1=1`;
177
307
  const params = [];
178
308
  if (lib) {
179
- sql += ` WHERE library = ?`;
309
+ sql += ` AND library = ?`;
180
310
  params.push(lib);
181
311
  }
182
- sql += ` ORDER BY library, path`;
312
+ if (source && source !== "all") {
313
+ sql += ` AND source = ?`;
314
+ params.push(source);
315
+ }
316
+ sql += ` ORDER BY source, library, path`;
183
317
  const stmt = db.prepare(sql);
184
318
  return stmt.all(...params);
185
319
  }
@@ -188,7 +322,7 @@ export function listDocs(lib) {
188
322
  */
189
323
  export function getDoc(path) {
190
324
  const db = openIndex();
191
- const stmt = db.prepare(`SELECT library, path, title, content FROM docs WHERE path = ?`);
325
+ const stmt = db.prepare(`SELECT library, path, title, content, source FROM docs WHERE path = ?`);
192
326
  return stmt.get(path);
193
327
  }
194
328
  /**
@@ -201,7 +335,10 @@ export function getStats() {
201
335
  const byLibStmt = db.prepare(`SELECT library, COUNT(*) as count FROM docs GROUP BY library`);
202
336
  const byLibRows = byLibStmt.all();
203
337
  const byLibrary = Object.fromEntries(byLibRows.map((row) => [row.library, row.count]));
204
- return { totalDocs: total, byLibrary };
338
+ const bySourceStmt = db.prepare(`SELECT source, COUNT(*) as count FROM docs GROUP BY source`);
339
+ const bySourceRows = bySourceStmt.all();
340
+ const bySource = Object.fromEntries(bySourceRows.map((row) => [row.source, row.count]));
341
+ return { totalDocs: total, byLibrary, bySource };
205
342
  }
206
343
  /**
207
344
  * Run a function with the index open, closing it automatically afterward.
@@ -6,15 +6,47 @@ import { LibraryConfig } from "../config.js";
6
6
  * Clone a library repository (sparse checkout, docs only)
7
7
  */
8
8
  export declare function cloneLibrary(lib: LibraryConfig): Promise<void>;
9
+ /**
10
+ * Clone TCA examples (CaseStudies, SyncUps, etc.)
11
+ */
12
+ export declare function cloneExamples(): Promise<void>;
13
+ /**
14
+ * Clone episode code samples
15
+ */
16
+ export declare function cloneEpisodes(): Promise<void>;
9
17
  /**
10
18
  * Update a library repository
11
19
  */
12
20
  export declare function updateLibrary(lib: LibraryConfig): Promise<boolean>;
21
+ /**
22
+ * Update examples repository
23
+ */
24
+ export declare function updateExamples(): Promise<boolean>;
25
+ /**
26
+ * Update episodes repository
27
+ */
28
+ export declare function updateEpisodes(): Promise<boolean>;
13
29
  /**
14
30
  * Get all local paths to a library's docs
15
31
  */
16
32
  export declare function getDocsPaths(lib: LibraryConfig): string[];
33
+ /**
34
+ * Get all local paths to examples
35
+ */
36
+ export declare function getExamplesPaths(): string[];
37
+ /**
38
+ * Get local path to episodes
39
+ */
40
+ export declare function getEpisodesPath(): string;
17
41
  /**
18
42
  * Check if a library is cloned
19
43
  */
20
44
  export declare function isLibraryCloned(lib: LibraryConfig): boolean;
45
+ /**
46
+ * Check if examples are cloned
47
+ */
48
+ export declare function areExamplesCloned(): boolean;
49
+ /**
50
+ * Check if episodes are cloned
51
+ */
52
+ export declare function areEpisodesCloned(): boolean;
package/dist/lib/repos.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { simpleGit } from "simple-git";
5
5
  import { existsSync, mkdirSync } from "fs";
6
6
  import { join } from "path";
7
- import { PATHS } from "../config.js";
7
+ import { PATHS, EXAMPLES_CONFIG, EPISODES_CONFIG } from "../config.js";
8
8
  /**
9
9
  * Clone a library repository (sparse checkout, docs only)
10
10
  */
@@ -34,6 +34,58 @@ export async function cloneLibrary(lib) {
34
34
  await repoGit.raw(["sparse-checkout", "set", ...lib.docsPaths]);
35
35
  console.log(` ✓ Cloned ${lib.shortName}`);
36
36
  }
37
+ /**
38
+ * Clone TCA examples (CaseStudies, SyncUps, etc.)
39
+ */
40
+ export async function cloneExamples() {
41
+ const repoDir = join(PATHS.reposDir, EXAMPLES_CONFIG.name);
42
+ // Ensure repos directory exists
43
+ if (!existsSync(PATHS.reposDir)) {
44
+ mkdirSync(PATHS.reposDir, { recursive: true });
45
+ }
46
+ if (existsSync(repoDir)) {
47
+ console.log(` Repository already exists: ${EXAMPLES_CONFIG.name}`);
48
+ return;
49
+ }
50
+ console.log(` Cloning ${EXAMPLES_CONFIG.repo} (examples)...`);
51
+ const git = simpleGit();
52
+ // Sparse checkout: only download examples directories
53
+ await git.clone(`https://github.com/${EXAMPLES_CONFIG.repo}.git`, repoDir, [
54
+ "--depth",
55
+ "1",
56
+ "--filter=blob:none",
57
+ "--sparse",
58
+ ]);
59
+ const repoGit = simpleGit(repoDir);
60
+ // Set up sparse checkout to only get the examples folders
61
+ await repoGit.raw(["sparse-checkout", "init", "--cone"]);
62
+ await repoGit.raw(["sparse-checkout", "set", ...EXAMPLES_CONFIG.paths]);
63
+ console.log(` ✓ Cloned examples`);
64
+ }
65
+ /**
66
+ * Clone episode code samples
67
+ */
68
+ export async function cloneEpisodes() {
69
+ const repoDir = join(PATHS.reposDir, EPISODES_CONFIG.name);
70
+ // Ensure repos directory exists
71
+ if (!existsSync(PATHS.reposDir)) {
72
+ mkdirSync(PATHS.reposDir, { recursive: true });
73
+ }
74
+ if (existsSync(repoDir)) {
75
+ console.log(` Repository already exists: ${EPISODES_CONFIG.name}`);
76
+ return;
77
+ }
78
+ console.log(` Cloning ${EPISODES_CONFIG.repo} (episodes)...`);
79
+ console.log(` ⚠ Note: This may take a while (350+ episodes)...`);
80
+ const git = simpleGit();
81
+ // Full clone with depth 1 and blob filter for efficiency
82
+ await git.clone(`https://github.com/${EPISODES_CONFIG.repo}.git`, repoDir, [
83
+ "--depth",
84
+ "1",
85
+ "--filter=blob:none",
86
+ ]);
87
+ console.log(` ✓ Cloned episodes`);
88
+ }
37
89
  /**
38
90
  * Update a library repository
39
91
  */
@@ -61,12 +113,78 @@ export async function updateLibrary(lib) {
61
113
  return false;
62
114
  }
63
115
  }
116
+ /**
117
+ * Update examples repository
118
+ */
119
+ export async function updateExamples() {
120
+ const repoDir = join(PATHS.reposDir, EXAMPLES_CONFIG.name);
121
+ if (!existsSync(repoDir)) {
122
+ console.log(` Examples not found. Run 'pf-docs init --examples' first.`);
123
+ return false;
124
+ }
125
+ console.log(` Updating examples...`);
126
+ const git = simpleGit(repoDir);
127
+ try {
128
+ const pullResult = await git.pull();
129
+ if (pullResult.summary.changes > 0) {
130
+ console.log(` ✓ Updated examples (${pullResult.summary.changes} changes)`);
131
+ return true;
132
+ }
133
+ else {
134
+ console.log(` ✓ Examples are up to date`);
135
+ return false;
136
+ }
137
+ }
138
+ catch (error) {
139
+ console.error(` ✗ Failed to update examples:`, error);
140
+ return false;
141
+ }
142
+ }
143
+ /**
144
+ * Update episodes repository
145
+ */
146
+ export async function updateEpisodes() {
147
+ const repoDir = join(PATHS.reposDir, EPISODES_CONFIG.name);
148
+ if (!existsSync(repoDir)) {
149
+ console.log(` Episodes not found. Run 'pf-docs init --episodes' first.`);
150
+ return false;
151
+ }
152
+ console.log(` Updating episodes...`);
153
+ const git = simpleGit(repoDir);
154
+ try {
155
+ const pullResult = await git.pull();
156
+ if (pullResult.summary.changes > 0) {
157
+ console.log(` ✓ Updated episodes (${pullResult.summary.changes} changes)`);
158
+ return true;
159
+ }
160
+ else {
161
+ console.log(` ✓ Episodes are up to date`);
162
+ return false;
163
+ }
164
+ }
165
+ catch (error) {
166
+ console.error(` ✗ Failed to update episodes:`, error);
167
+ return false;
168
+ }
169
+ }
64
170
  /**
65
171
  * Get all local paths to a library's docs
66
172
  */
67
173
  export function getDocsPaths(lib) {
68
174
  return lib.docsPaths.map((docsPath) => join(PATHS.reposDir, lib.name, docsPath));
69
175
  }
176
+ /**
177
+ * Get all local paths to examples
178
+ */
179
+ export function getExamplesPaths() {
180
+ return EXAMPLES_CONFIG.paths.map((path) => join(PATHS.reposDir, EXAMPLES_CONFIG.name, path));
181
+ }
182
+ /**
183
+ * Get local path to episodes
184
+ */
185
+ export function getEpisodesPath() {
186
+ return join(PATHS.reposDir, EPISODES_CONFIG.name);
187
+ }
70
188
  /**
71
189
  * Check if a library is cloned
72
190
  */
@@ -74,3 +192,17 @@ export function isLibraryCloned(lib) {
74
192
  const repoDir = join(PATHS.reposDir, lib.name);
75
193
  return existsSync(repoDir);
76
194
  }
195
+ /**
196
+ * Check if examples are cloned
197
+ */
198
+ export function areExamplesCloned() {
199
+ const repoDir = join(PATHS.reposDir, EXAMPLES_CONFIG.name);
200
+ return existsSync(repoDir);
201
+ }
202
+ /**
203
+ * Check if episodes are cloned
204
+ */
205
+ export function areEpisodesCloned() {
206
+ const repoDir = join(PATHS.reposDir, EPISODES_CONFIG.name);
207
+ return existsSync(repoDir);
208
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pointfree-docs",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "CLI tool for searching Point-Free library documentation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,10 +33,10 @@
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
35
  "better-sqlite3": "^11.0.0",
36
+ "chalk": "^5.3.0",
36
37
  "commander": "^12.0.0",
37
38
  "glob": "^10.3.0",
38
- "simple-git": "^3.22.0",
39
- "chalk": "^5.3.0"
39
+ "simple-git": "^3.22.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/better-sqlite3": "^7.6.9",