pointfree-docs 0.1.0 ā 0.2.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 +67 -7
- package/dist/cli.js +9 -1
- package/dist/commands/get.d.ts +2 -0
- package/dist/commands/get.js +59 -2
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +78 -13
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +59 -19
- package/dist/commands/search.d.ts +1 -0
- package/dist/commands/search.js +27 -4
- package/dist/commands/stats.js +32 -5
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +33 -7
- package/dist/config.d.ts +32 -1
- package/dist/config.js +34 -1
- package/dist/lib/format.d.ts +12 -0
- package/dist/lib/format.js +30 -0
- package/dist/lib/index.d.ts +14 -2
- package/dist/lib/index.js +162 -25
- package/dist/lib/repos.d.ts +32 -0
- package/dist/lib/repos.js +133 -1
- package/package.json +3 -3
package/dist/commands/update.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { LIBRARIES, getLibrary } from "../config.js";
|
|
6
|
-
import { updateLibrary, isLibraryCloned } from "../lib/repos.js";
|
|
7
|
-
import { indexLibrary, openIndex, closeIndex } from "../lib/index.js";
|
|
6
|
+
import { updateLibrary, updateExamples, updateEpisodes, isLibraryCloned, areExamplesCloned, areEpisodesCloned } from "../lib/repos.js";
|
|
7
|
+
import { indexLibrary, indexExamples, indexEpisodes, openIndex, closeIndex } from "../lib/index.js";
|
|
8
8
|
export async function updateCommand(options) {
|
|
9
9
|
console.log(chalk.bold("\nš Updating Point-Free Documentation\n"));
|
|
10
10
|
// Determine which libraries to update
|
|
@@ -24,27 +24,53 @@ export async function updateCommand(options) {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// Check for examples/episodes
|
|
28
|
+
const shouldUpdateExamples = options.examples && areExamplesCloned();
|
|
29
|
+
const shouldUpdateEpisodes = options.episodes && areEpisodesCloned();
|
|
30
|
+
if (options.examples && !areExamplesCloned()) {
|
|
31
|
+
console.log(chalk.yellow(` ā Examples not initialized. Run 'pf-docs init --examples' first.`));
|
|
32
|
+
}
|
|
33
|
+
if (options.episodes && !areEpisodesCloned()) {
|
|
34
|
+
console.log(chalk.yellow(` ā Episodes not initialized. Run 'pf-docs init --episodes' first.`));
|
|
35
|
+
}
|
|
36
|
+
if (librariesToUpdate.length === 0 && !shouldUpdateExamples && !shouldUpdateEpisodes) {
|
|
37
|
+
console.log(chalk.yellow("Nothing to update. Run 'pf-docs init' first."));
|
|
29
38
|
return;
|
|
30
39
|
}
|
|
31
40
|
// Update repositories
|
|
32
41
|
console.log(chalk.bold("Pulling latest changes..."));
|
|
33
42
|
const updatedLibs = [];
|
|
43
|
+
let examplesUpdated = false;
|
|
44
|
+
let episodesUpdated = false;
|
|
34
45
|
for (const lib of librariesToUpdate) {
|
|
35
46
|
const wasUpdated = await updateLibrary(lib);
|
|
36
47
|
if (wasUpdated) {
|
|
37
48
|
updatedLibs.push(lib);
|
|
38
49
|
}
|
|
39
50
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
if (shouldUpdateExamples) {
|
|
52
|
+
examplesUpdated = await updateExamples();
|
|
53
|
+
}
|
|
54
|
+
if (shouldUpdateEpisodes) {
|
|
55
|
+
episodesUpdated = await updateEpisodes();
|
|
56
|
+
}
|
|
57
|
+
// Re-index updated sources
|
|
58
|
+
const hasUpdates = updatedLibs.length > 0 || examplesUpdated || episodesUpdated;
|
|
59
|
+
if (hasUpdates) {
|
|
60
|
+
console.log(chalk.bold("\nRe-indexing updated sources..."));
|
|
43
61
|
openIndex();
|
|
44
62
|
for (const lib of updatedLibs) {
|
|
45
63
|
const count = indexLibrary(lib);
|
|
46
64
|
console.log(` ā Re-indexed ${lib.shortName}: ${count} documents`);
|
|
47
65
|
}
|
|
66
|
+
if (examplesUpdated) {
|
|
67
|
+
const count = indexExamples();
|
|
68
|
+
console.log(` ā Re-indexed examples: ${count} files`);
|
|
69
|
+
}
|
|
70
|
+
if (episodesUpdated) {
|
|
71
|
+
const count = indexEpisodes();
|
|
72
|
+
console.log(` ā Re-indexed episodes: ${count} files`);
|
|
73
|
+
}
|
|
48
74
|
closeIndex();
|
|
49
75
|
}
|
|
50
76
|
console.log(chalk.green(`\nā
Update complete!\n`));
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Configuration for Point-Free libraries
|
|
2
|
+
* Configuration for Point-Free libraries and example sources
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Source types for documentation
|
|
6
|
+
*/
|
|
7
|
+
export type SourceType = "docs" | "examples" | "episodes";
|
|
4
8
|
export interface LibraryConfig {
|
|
5
9
|
name: string;
|
|
6
10
|
shortName: string;
|
|
@@ -8,11 +12,34 @@ export interface LibraryConfig {
|
|
|
8
12
|
docsPaths: string[];
|
|
9
13
|
description: string;
|
|
10
14
|
}
|
|
15
|
+
export interface ExamplesConfig {
|
|
16
|
+
name: string;
|
|
17
|
+
shortName: string;
|
|
18
|
+
repo: string;
|
|
19
|
+
paths: string[];
|
|
20
|
+
description: string;
|
|
21
|
+
filePatterns: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface EpisodesConfig {
|
|
24
|
+
name: string;
|
|
25
|
+
shortName: string;
|
|
26
|
+
repo: string;
|
|
27
|
+
description: string;
|
|
28
|
+
filePatterns: string[];
|
|
29
|
+
}
|
|
11
30
|
/**
|
|
12
31
|
* All available Point-Free libraries
|
|
13
32
|
* Add or remove libraries here to customize what's indexed
|
|
14
33
|
*/
|
|
15
34
|
export declare const LIBRARIES: LibraryConfig[];
|
|
35
|
+
/**
|
|
36
|
+
* TCA Examples configuration (CaseStudies, SyncUps, etc.)
|
|
37
|
+
*/
|
|
38
|
+
export declare const EXAMPLES_CONFIG: ExamplesConfig;
|
|
39
|
+
/**
|
|
40
|
+
* Episode code samples configuration
|
|
41
|
+
*/
|
|
42
|
+
export declare const EPISODES_CONFIG: EpisodesConfig;
|
|
16
43
|
/**
|
|
17
44
|
* Get library config by short name
|
|
18
45
|
*/
|
|
@@ -21,6 +48,10 @@ export declare function getLibrary(shortName: string): LibraryConfig | undefined
|
|
|
21
48
|
* All library short names
|
|
22
49
|
*/
|
|
23
50
|
export declare const LIBRARY_NAMES: string[];
|
|
51
|
+
/**
|
|
52
|
+
* All valid source types
|
|
53
|
+
*/
|
|
54
|
+
export declare const SOURCE_TYPES: SourceType[];
|
|
24
55
|
export declare const PATHS: {
|
|
25
56
|
dataDir: string;
|
|
26
57
|
reposDir: string;
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Configuration for Point-Free libraries
|
|
2
|
+
* Configuration for Point-Free libraries and example sources
|
|
3
3
|
*/
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
/**
|
|
@@ -100,6 +100,35 @@ export const LIBRARIES = [
|
|
|
100
100
|
description: "Runtime warnings & assertions",
|
|
101
101
|
},
|
|
102
102
|
];
|
|
103
|
+
/**
|
|
104
|
+
* TCA Examples configuration (CaseStudies, SyncUps, etc.)
|
|
105
|
+
*/
|
|
106
|
+
export const EXAMPLES_CONFIG = {
|
|
107
|
+
name: "tca-examples",
|
|
108
|
+
shortName: "examples",
|
|
109
|
+
repo: "pointfreeco/swift-composable-architecture",
|
|
110
|
+
paths: [
|
|
111
|
+
"Examples/CaseStudies",
|
|
112
|
+
"Examples/SyncUps",
|
|
113
|
+
"Examples/Todos",
|
|
114
|
+
"Examples/VoiceMemos",
|
|
115
|
+
"Examples/Search",
|
|
116
|
+
"Examples/TicTacToe",
|
|
117
|
+
"Examples/SpeechRecognition",
|
|
118
|
+
],
|
|
119
|
+
description: "TCA example apps and case studies",
|
|
120
|
+
filePatterns: ["**/*.swift"],
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Episode code samples configuration
|
|
124
|
+
*/
|
|
125
|
+
export const EPISODES_CONFIG = {
|
|
126
|
+
name: "episode-code-samples",
|
|
127
|
+
shortName: "episodes",
|
|
128
|
+
repo: "pointfreeco/episode-code-samples",
|
|
129
|
+
description: "Point-Free episode code samples (350+ episodes)",
|
|
130
|
+
filePatterns: ["**/*.swift", "**/*.playground/**/*.swift"],
|
|
131
|
+
};
|
|
103
132
|
/**
|
|
104
133
|
* Get library config by short name
|
|
105
134
|
*/
|
|
@@ -110,6 +139,10 @@ export function getLibrary(shortName) {
|
|
|
110
139
|
* All library short names
|
|
111
140
|
*/
|
|
112
141
|
export const LIBRARY_NAMES = LIBRARIES.map((lib) => lib.shortName);
|
|
142
|
+
/**
|
|
143
|
+
* All valid source types
|
|
144
|
+
*/
|
|
145
|
+
export const SOURCE_TYPES = ["docs", "examples", "episodes"];
|
|
113
146
|
/**
|
|
114
147
|
* Paths configuration
|
|
115
148
|
*/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatting utilities
|
|
3
|
+
*/
|
|
4
|
+
import { SourceType } from "../config.js";
|
|
5
|
+
/**
|
|
6
|
+
* Get source type label with color and brackets for search/list results
|
|
7
|
+
*/
|
|
8
|
+
export declare function getSourceLabel(source: SourceType): string;
|
|
9
|
+
/**
|
|
10
|
+
* Get source type name with color (for stats display)
|
|
11
|
+
*/
|
|
12
|
+
export declare function getSourceName(source: SourceType): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatting utilities
|
|
3
|
+
*/
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
/**
|
|
6
|
+
* Get source type label with color and brackets for search/list results
|
|
7
|
+
*/
|
|
8
|
+
export function getSourceLabel(source) {
|
|
9
|
+
switch (source) {
|
|
10
|
+
case "docs":
|
|
11
|
+
return chalk.cyan("[DOC]");
|
|
12
|
+
case "examples":
|
|
13
|
+
return chalk.magenta("[EXAMPLE]");
|
|
14
|
+
case "episodes":
|
|
15
|
+
return chalk.yellow("[EPISODE]");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get source type name with color (for stats display)
|
|
20
|
+
*/
|
|
21
|
+
export function getSourceName(source) {
|
|
22
|
+
switch (source) {
|
|
23
|
+
case "docs":
|
|
24
|
+
return chalk.cyan("docs");
|
|
25
|
+
case "examples":
|
|
26
|
+
return chalk.magenta("examples");
|
|
27
|
+
case "episodes":
|
|
28
|
+
return chalk.yellow("episodes");
|
|
29
|
+
}
|
|
30
|
+
}
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Search index using SQLite FTS5 for full-text search
|
|
3
3
|
*/
|
|
4
4
|
import Database from "better-sqlite3";
|
|
5
|
-
import { LibraryConfig } from "../config.js";
|
|
5
|
+
import { LibraryConfig, SourceType } from "../config.js";
|
|
6
6
|
export interface DocEntry {
|
|
7
7
|
library: string;
|
|
8
8
|
path: string;
|
|
9
9
|
title: string;
|
|
10
|
+
source: SourceType;
|
|
10
11
|
}
|
|
11
12
|
export interface DocWithContent extends DocEntry {
|
|
12
13
|
content: string;
|
|
@@ -14,6 +15,7 @@ export interface DocWithContent extends DocEntry {
|
|
|
14
15
|
export interface IndexStats {
|
|
15
16
|
totalDocs: number;
|
|
16
17
|
byLibrary: Record<string, number>;
|
|
18
|
+
bySource: Record<SourceType, number>;
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
21
|
* Initialize or open the search index database
|
|
@@ -23,6 +25,14 @@ export declare function openIndex(): Database.Database;
|
|
|
23
25
|
* Index all documentation files for a library
|
|
24
26
|
*/
|
|
25
27
|
export declare function indexLibrary(lib: LibraryConfig): number;
|
|
28
|
+
/**
|
|
29
|
+
* Index TCA examples (CaseStudies, etc.)
|
|
30
|
+
*/
|
|
31
|
+
export declare function indexExamples(): number;
|
|
32
|
+
/**
|
|
33
|
+
* Index episode code samples
|
|
34
|
+
*/
|
|
35
|
+
export declare function indexEpisodes(): number;
|
|
26
36
|
/**
|
|
27
37
|
* Search the index
|
|
28
38
|
*/
|
|
@@ -32,15 +42,17 @@ export interface SearchResult {
|
|
|
32
42
|
title: string;
|
|
33
43
|
snippet: string;
|
|
34
44
|
score: number;
|
|
45
|
+
source: SourceType;
|
|
35
46
|
}
|
|
36
47
|
export declare function search(query: string, options?: {
|
|
37
48
|
lib?: string;
|
|
38
49
|
limit?: number;
|
|
50
|
+
source?: SourceType | "all";
|
|
39
51
|
}): SearchResult[];
|
|
40
52
|
/**
|
|
41
53
|
* List all indexed documents
|
|
42
54
|
*/
|
|
43
|
-
export declare function listDocs(lib?: string): DocEntry[];
|
|
55
|
+
export declare function listDocs(lib?: string, source?: SourceType | "all"): DocEntry[];
|
|
44
56
|
/**
|
|
45
57
|
* Get a specific document by path
|
|
46
58
|
*/
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
|
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 += `
|
|
309
|
+
sql += ` AND library = ?`;
|
|
180
310
|
params.push(lib);
|
|
181
311
|
}
|
|
182
|
-
|
|
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
|
-
|
|
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.
|
package/dist/lib/repos.d.ts
CHANGED
|
@@ -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;
|