-1
.tangled/workflows/test.yml
-1
.tangled/workflows/test.yml
+74
.wispignore.json
+74
.wispignore.json
···
1
+
{
2
+
"version": "1.0.0",
3
+
"description": "Default ignore patterns for wisp.place uploads",
4
+
"patterns": [
5
+
".git",
6
+
".git/**",
7
+
".github",
8
+
".github/**",
9
+
".gitlab",
10
+
".gitlab/**",
11
+
".DS_Store",
12
+
".wisp.metadata.json",
13
+
".env",
14
+
".env.*",
15
+
"node_modules",
16
+
"node_modules/**",
17
+
"Thumbs.db",
18
+
"desktop.ini",
19
+
"._*",
20
+
".Spotlight-V100",
21
+
".Spotlight-V100/**",
22
+
".Trashes",
23
+
".Trashes/**",
24
+
".fseventsd",
25
+
".fseventsd/**",
26
+
".cache",
27
+
".cache/**",
28
+
".temp",
29
+
".temp/**",
30
+
".tmp",
31
+
".tmp/**",
32
+
"__pycache__",
33
+
"__pycache__/**",
34
+
"*.pyc",
35
+
".venv",
36
+
".venv/**",
37
+
"venv",
38
+
"venv/**",
39
+
"env",
40
+
"env/**",
41
+
"*.swp",
42
+
"*.swo",
43
+
"*~",
44
+
".tangled",
45
+
".tangled/**"
46
+
],
47
+
"comments": {
48
+
".git": "Version control - thousands of files",
49
+
".github": "GitHub workflows and configuration",
50
+
".gitlab": "GitLab CI/CD configuration",
51
+
".DS_Store": "macOS metadata - can leak info",
52
+
".wisp.metadata.json": "Wisp internal metadata",
53
+
".env": "Environment variables with secrets",
54
+
"node_modules": "Dependency folder - can be 100,000+ files",
55
+
"Thumbs.db": "Windows thumbnail cache",
56
+
"desktop.ini": "Windows folder config",
57
+
"._*": "macOS resource fork files",
58
+
".Spotlight-V100": "macOS Spotlight index",
59
+
".Trashes": "macOS trash folder",
60
+
".fseventsd": "macOS filesystem events",
61
+
".cache": "Cache directories",
62
+
".temp": "Temporary directories",
63
+
".tmp": "Temporary directories",
64
+
"__pycache__": "Python cache",
65
+
"*.pyc": "Python compiled files",
66
+
".venv": "Python virtual environments",
67
+
"venv": "Python virtual environments",
68
+
"env": "Python virtual environments",
69
+
"*.swp": "Vim swap files",
70
+
"*.swo": "Vim swap files",
71
+
"*~": "Editor backup files",
72
+
".tangled": "Tangled directory"
73
+
}
74
+
}
+9
-8
apps/main-app/package.json
+9
-8
apps/main-app/package.json
···
10
10
"screenshot": "bun run scripts/screenshot-sites.ts"
11
11
},
12
12
"dependencies": {
13
-
"@wisp/lexicons": "workspace:*",
14
-
"@wisp/constants": "workspace:*",
15
-
"@wisp/observability": "workspace:*",
16
-
"@wisp/atproto-utils": "workspace:*",
17
-
"@wisp/database": "workspace:*",
18
-
"@wisp/fs-utils": "workspace:*",
19
13
"@atproto/api": "^0.17.3",
20
14
"@atproto/common-web": "^0.4.5",
21
15
"@atproto/jwk-jose": "^0.1.11",
···
34
28
"@radix-ui/react-slot": "^1.2.3",
35
29
"@radix-ui/react-tabs": "^1.1.13",
36
30
"@tanstack/react-query": "^5.90.2",
31
+
"@wisp/atproto-utils": "workspace:*",
32
+
"@wisp/constants": "workspace:*",
33
+
"@wisp/database": "workspace:*",
34
+
"@wisp/fs-utils": "workspace:*",
35
+
"@wisp/lexicons": "workspace:*",
36
+
"@wisp/observability": "workspace:*",
37
37
"actor-typeahead": "^0.1.1",
38
38
"atproto-ui": "^0.11.3",
39
+
"bun-plugin-tailwind": "^0.1.2",
39
40
"class-variance-authority": "^0.7.1",
40
41
"clsx": "^2.1.1",
41
42
"elysia": "latest",
43
+
"ignore": "^7.0.5",
42
44
"iron-session": "^8.0.4",
43
45
"lucide-react": "^0.546.0",
44
46
"multiformats": "^13.4.1",
···
48
50
"tailwind-merge": "^3.3.1",
49
51
"tailwindcss": "4",
50
52
"tw-animate-css": "^1.4.0",
51
-
"zlib": "^1.0.5",
52
-
"bun-plugin-tailwind": "^0.1.2"
53
+
"zlib": "^1.0.5"
53
54
},
54
55
"devDependencies": {
55
56
"@types/react": "^19.2.2",
+127
apps/main-app/src/lib/ignore-patterns.ts
+127
apps/main-app/src/lib/ignore-patterns.ts
···
1
+
import ignore, { type Ignore } from 'ignore'
2
+
import { readFileSync, existsSync } from 'fs'
3
+
import { join } from 'path'
4
+
5
+
interface IgnoreConfig {
6
+
version: string
7
+
description: string
8
+
patterns: string[]
9
+
}
10
+
11
+
/**
12
+
* Load default ignore patterns from the .wispignore.json file in the monorepo root
13
+
*/
14
+
function loadDefaultPatterns(): string[] {
15
+
try {
16
+
// Path to the default ignore patterns JSON file (monorepo root, 3 levels up from this file)
17
+
const defaultJsonPath = join(__dirname, '../../../../.wispignore.json')
18
+
19
+
if (!existsSync(defaultJsonPath)) {
20
+
console.warn('⚠️ Default .wispignore.json not found, using hardcoded patterns')
21
+
return getHardcodedPatterns()
22
+
}
23
+
24
+
const contents = readFileSync(defaultJsonPath, 'utf-8')
25
+
const config: IgnoreConfig = JSON.parse(contents)
26
+
return config.patterns
27
+
} catch (error) {
28
+
console.error('Failed to load default ignore patterns:', error)
29
+
return getHardcodedPatterns()
30
+
}
31
+
}
32
+
33
+
/**
34
+
* Hardcoded fallback patterns (same as in .wispignore.json)
35
+
*/
36
+
function getHardcodedPatterns(): string[] {
37
+
return [
38
+
'.git',
39
+
'.git/**',
40
+
'.github',
41
+
'.github/**',
42
+
'.gitlab',
43
+
'.gitlab/**',
44
+
'.DS_Store',
45
+
'.wisp.metadata.json',
46
+
'.env',
47
+
'.env.*',
48
+
'node_modules',
49
+
'node_modules/**',
50
+
'Thumbs.db',
51
+
'desktop.ini',
52
+
'._*',
53
+
'.Spotlight-V100',
54
+
'.Spotlight-V100/**',
55
+
'.Trashes',
56
+
'.Trashes/**',
57
+
'.fseventsd',
58
+
'.fseventsd/**',
59
+
'.cache',
60
+
'.cache/**',
61
+
'.temp',
62
+
'.temp/**',
63
+
'.tmp',
64
+
'.tmp/**',
65
+
'__pycache__',
66
+
'__pycache__/**',
67
+
'*.pyc',
68
+
'.venv',
69
+
'.venv/**',
70
+
'venv',
71
+
'venv/**',
72
+
'env',
73
+
'env/**',
74
+
'*.swp',
75
+
'*.swo',
76
+
'*~',
77
+
'.tangled',
78
+
'.tangled/**',
79
+
]
80
+
}
81
+
82
+
/**
83
+
* Load custom ignore patterns from a .wispignore file
84
+
* @param wispignoreContent - Content of the .wispignore file (one pattern per line)
85
+
*/
86
+
function loadWispignorePatterns(wispignoreContent: string): string[] {
87
+
return wispignoreContent
88
+
.split('\n')
89
+
.map(line => line.trim())
90
+
.filter(line => line && !line.startsWith('#')) // Skip empty lines and comments
91
+
}
92
+
93
+
/**
94
+
* Create an ignore matcher
95
+
* @param customPatterns - Optional custom patterns from a .wispignore file
96
+
*/
97
+
export function createIgnoreMatcher(customPatterns?: string[]): Ignore {
98
+
const ig = ignore()
99
+
100
+
// Add default patterns
101
+
const defaultPatterns = loadDefaultPatterns()
102
+
ig.add(defaultPatterns)
103
+
104
+
// Add custom patterns if provided
105
+
if (customPatterns && customPatterns.length > 0) {
106
+
ig.add(customPatterns)
107
+
console.log(`Loaded ${customPatterns.length} custom patterns from .wispignore`)
108
+
}
109
+
110
+
return ig
111
+
}
112
+
113
+
/**
114
+
* Check if a file path should be ignored
115
+
* @param matcher - The ignore matcher
116
+
* @param filePath - The file path to check (relative to site root)
117
+
*/
118
+
export function shouldIgnore(matcher: Ignore, filePath: string): boolean {
119
+
return matcher.ignores(filePath)
120
+
}
121
+
122
+
/**
123
+
* Parse .wispignore content and return patterns
124
+
*/
125
+
export function parseWispignore(content: string): string[] {
126
+
return loadWispignorePatterns(content)
127
+
}
+22
-97
apps/main-app/src/routes/wisp.ts
+22
-97
apps/main-app/src/routes/wisp.ts
···
34
34
failUploadJob,
35
35
addJobListener
36
36
} from '../lib/upload-jobs'
37
+
import { createIgnoreMatcher, shouldIgnore, parseWispignore } from '../lib/ignore-patterns'
38
+
import type { Ignore } from 'ignore'
37
39
38
40
const logger = createLogger('main-app')
39
41
···
145
147
}
146
148
}
147
149
150
+
// Check for .wispignore file in uploaded files
151
+
let customIgnorePatterns: string[] = [];
152
+
const wispignoreFile = fileArray.find(f => f && f.name && f.name.endsWith('.wispignore'));
153
+
if (wispignoreFile) {
154
+
try {
155
+
const content = await wispignoreFile.text();
156
+
customIgnorePatterns = parseWispignore(content);
157
+
console.log(`Found .wispignore file with ${customIgnorePatterns.length} custom patterns`);
158
+
} catch (err) {
159
+
console.warn('Failed to parse .wispignore file:', err);
160
+
}
161
+
}
162
+
163
+
// Create ignore matcher with default and custom patterns
164
+
const ignoreMatcher = createIgnoreMatcher(customIgnorePatterns);
165
+
148
166
// Convert File objects to UploadedFile format
149
167
const uploadedFiles: UploadedFile[] = [];
150
168
const skippedFiles: Array<{ name: string; reason: string }> = [];
···
171
189
currentFile: file.name
172
190
});
173
191
174
-
// Skip unwanted files and directories
192
+
// Skip files that match ignore patterns
175
193
const normalizedPath = file.name.replace(/^[^\/]*\//, '');
176
-
const fileName = normalizedPath.split('/').pop() || '';
177
-
const pathParts = normalizedPath.split('/');
178
194
179
-
// .git directory (version control - thousands of files)
180
-
if (normalizedPath.startsWith('.git/') || normalizedPath === '.git') {
181
-
console.log(`Skipping .git file: ${file.name}`);
182
-
skippedFiles.push({
183
-
name: file.name,
184
-
reason: '.git directory excluded'
185
-
});
186
-
continue;
187
-
}
188
-
189
-
// .DS_Store (macOS metadata - can leak info)
190
-
if (fileName === '.DS_Store') {
191
-
console.log(`Skipping .DS_Store file: ${file.name}`);
195
+
if (shouldIgnore(ignoreMatcher, normalizedPath)) {
196
+
console.log(`Skipping ignored file: ${file.name}`);
192
197
skippedFiles.push({
193
198
name: file.name,
194
-
reason: '.DS_Store file excluded'
195
-
});
196
-
continue;
197
-
}
198
-
199
-
// .env files (environment variables with secrets)
200
-
if (fileName.startsWith('.env')) {
201
-
console.log(`Skipping .env file: ${file.name}`);
202
-
skippedFiles.push({
203
-
name: file.name,
204
-
reason: 'environment files excluded for security'
205
-
});
206
-
continue;
207
-
}
208
-
209
-
// node_modules (dependency folder - can be 100,000+ files)
210
-
if (pathParts.includes('node_modules')) {
211
-
console.log(`Skipping node_modules file: ${file.name}`);
212
-
skippedFiles.push({
213
-
name: file.name,
214
-
reason: 'node_modules excluded'
215
-
});
216
-
continue;
217
-
}
218
-
219
-
// OS metadata files
220
-
if (fileName === 'Thumbs.db' || fileName === 'desktop.ini' || fileName.startsWith('._')) {
221
-
console.log(`Skipping OS metadata file: ${file.name}`);
222
-
skippedFiles.push({
223
-
name: file.name,
224
-
reason: 'OS metadata file excluded'
225
-
});
226
-
continue;
227
-
}
228
-
229
-
// macOS system directories
230
-
if (pathParts.includes('.Spotlight-V100') || pathParts.includes('.Trashes') || pathParts.includes('.fseventsd')) {
231
-
console.log(`Skipping macOS system file: ${file.name}`);
232
-
skippedFiles.push({
233
-
name: file.name,
234
-
reason: 'macOS system directory excluded'
235
-
});
236
-
continue;
237
-
}
238
-
239
-
// Cache and temp directories
240
-
if (pathParts.some(part => part === '.cache' || part === '.temp' || part === '.tmp')) {
241
-
console.log(`Skipping cache/temp file: ${file.name}`);
242
-
skippedFiles.push({
243
-
name: file.name,
244
-
reason: 'cache/temp directory excluded'
245
-
});
246
-
continue;
247
-
}
248
-
249
-
// Python cache
250
-
if (pathParts.includes('__pycache__') || fileName.endsWith('.pyc')) {
251
-
console.log(`Skipping Python cache file: ${file.name}`);
252
-
skippedFiles.push({
253
-
name: file.name,
254
-
reason: 'Python cache excluded'
255
-
});
256
-
continue;
257
-
}
258
-
259
-
// Python virtual environments
260
-
if (pathParts.some(part => part === '.venv' || part === 'venv' || part === 'env')) {
261
-
console.log(`Skipping Python venv file: ${file.name}`);
262
-
skippedFiles.push({
263
-
name: file.name,
264
-
reason: 'Python virtual environment excluded'
265
-
});
266
-
continue;
267
-
}
268
-
269
-
// Editor swap files
270
-
if (fileName.endsWith('.swp') || fileName.endsWith('.swo') || fileName.endsWith('~')) {
271
-
console.log(`Skipping editor swap file: ${file.name}`);
272
-
skippedFiles.push({
273
-
name: file.name,
274
-
reason: 'editor swap file excluded'
199
+
reason: 'matched ignore pattern'
275
200
});
276
201
continue;
277
202
}
+3
bun.lock
+3
bun.lock
···
73
73
"class-variance-authority": "^0.7.1",
74
74
"clsx": "^2.1.1",
75
75
"elysia": "latest",
76
+
"ignore": "^7.0.5",
76
77
"iron-session": "^8.0.4",
77
78
"lucide-react": "^0.546.0",
78
79
"multiformats": "^13.4.1",
···
768
769
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
769
770
770
771
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
772
+
773
+
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
771
774
772
775
"import-in-the-middle": ["import-in-the-middle@1.15.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA=="],
773
776
+61
-1
cli/Cargo.lock
+61
-1
cli/Cargo.lock
···
360
360
]
361
361
362
362
[[package]]
363
+
name = "bstr"
364
+
version = "1.12.1"
365
+
source = "registry+https://github.com/rust-lang/crates.io-index"
366
+
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
367
+
dependencies = [
368
+
"memchr",
369
+
"serde",
370
+
]
371
+
372
+
[[package]]
363
373
name = "btree-range-map"
364
374
version = "0.7.2"
365
375
source = "registry+https://github.com/rust-lang/crates.io-index"
···
674
684
version = "0.5.15"
675
685
source = "registry+https://github.com/rust-lang/crates.io-index"
676
686
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
687
+
dependencies = [
688
+
"crossbeam-utils",
689
+
]
690
+
691
+
[[package]]
692
+
name = "crossbeam-deque"
693
+
version = "0.8.6"
694
+
source = "registry+https://github.com/rust-lang/crates.io-index"
695
+
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
696
+
dependencies = [
697
+
"crossbeam-epoch",
698
+
"crossbeam-utils",
699
+
]
700
+
701
+
[[package]]
702
+
name = "crossbeam-epoch"
703
+
version = "0.9.18"
704
+
source = "registry+https://github.com/rust-lang/crates.io-index"
705
+
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
677
706
dependencies = [
678
707
"crossbeam-utils",
679
708
]
···
1209
1238
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
1210
1239
1211
1240
[[package]]
1241
+
name = "globset"
1242
+
version = "0.4.18"
1243
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1244
+
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
1245
+
dependencies = [
1246
+
"aho-corasick",
1247
+
"bstr",
1248
+
"log",
1249
+
"regex-automata",
1250
+
"regex-syntax",
1251
+
]
1252
+
1253
+
[[package]]
1212
1254
name = "gloo-storage"
1213
1255
version = "0.3.0"
1214
1256
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1649
1691
dependencies = [
1650
1692
"icu_normalizer",
1651
1693
"icu_properties",
1694
+
]
1695
+
1696
+
[[package]]
1697
+
name = "ignore"
1698
+
version = "0.4.25"
1699
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1700
+
checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
1701
+
dependencies = [
1702
+
"crossbeam-deque",
1703
+
"globset",
1704
+
"log",
1705
+
"memchr",
1706
+
"regex-automata",
1707
+
"same-file",
1708
+
"walkdir",
1709
+
"winapi-util",
1652
1710
]
1653
1711
1654
1712
[[package]]
···
4939
4997
4940
4998
[[package]]
4941
4999
name = "wisp-cli"
4942
-
version = "0.4.1"
5000
+
version = "0.4.2"
4943
5001
dependencies = [
4944
5002
"axum",
4945
5003
"base64 0.22.1",
···
4948
5006
"clap",
4949
5007
"flate2",
4950
5008
"futures",
5009
+
"globset",
5010
+
"ignore",
4951
5011
"jacquard",
4952
5012
"jacquard-api",
4953
5013
"jacquard-common",
+3
-1
cli/Cargo.toml
+3
-1
cli/Cargo.toml
+149
cli/src/ignore_patterns.rs
+149
cli/src/ignore_patterns.rs
···
1
+
use globset::{Glob, GlobSet, GlobSetBuilder};
2
+
use serde::{Deserialize, Serialize};
3
+
use std::path::Path;
4
+
use miette::IntoDiagnostic;
5
+
6
+
#[derive(Debug, Deserialize, Serialize)]
7
+
struct IgnoreConfig {
8
+
patterns: Vec<String>,
9
+
}
10
+
11
+
/// Load ignore patterns from the default .wispignore.json file
12
+
fn load_default_patterns() -> miette::Result<Vec<String>> {
13
+
// Path to the default ignore patterns JSON file (in the monorepo root)
14
+
let default_json_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../.wispignore.json");
15
+
16
+
match std::fs::read_to_string(default_json_path) {
17
+
Ok(contents) => {
18
+
let config: IgnoreConfig = serde_json::from_str(&contents).into_diagnostic()?;
19
+
Ok(config.patterns)
20
+
}
21
+
Err(_) => {
22
+
// If the default file doesn't exist, return hardcoded patterns as fallback
23
+
eprintln!("⚠️ Default .wispignore.json not found, using hardcoded patterns");
24
+
Ok(get_hardcoded_patterns())
25
+
}
26
+
}
27
+
}
28
+
29
+
/// Hardcoded fallback patterns (same as in .wispignore.json)
30
+
fn get_hardcoded_patterns() -> Vec<String> {
31
+
vec![
32
+
".git".to_string(),
33
+
".git/**".to_string(),
34
+
".github".to_string(),
35
+
".github/**".to_string(),
36
+
".gitlab".to_string(),
37
+
".gitlab/**".to_string(),
38
+
".DS_Store".to_string(),
39
+
".wisp.metadata.json".to_string(),
40
+
".env".to_string(),
41
+
".env.*".to_string(),
42
+
"node_modules".to_string(),
43
+
"node_modules/**".to_string(),
44
+
"Thumbs.db".to_string(),
45
+
"desktop.ini".to_string(),
46
+
"._*".to_string(),
47
+
".Spotlight-V100".to_string(),
48
+
".Spotlight-V100/**".to_string(),
49
+
".Trashes".to_string(),
50
+
".Trashes/**".to_string(),
51
+
".fseventsd".to_string(),
52
+
".fseventsd/**".to_string(),
53
+
".cache".to_string(),
54
+
".cache/**".to_string(),
55
+
".temp".to_string(),
56
+
".temp/**".to_string(),
57
+
".tmp".to_string(),
58
+
".tmp/**".to_string(),
59
+
"__pycache__".to_string(),
60
+
"__pycache__/**".to_string(),
61
+
"*.pyc".to_string(),
62
+
".venv".to_string(),
63
+
".venv/**".to_string(),
64
+
"venv".to_string(),
65
+
"venv/**".to_string(),
66
+
"env".to_string(),
67
+
"env/**".to_string(),
68
+
"*.swp".to_string(),
69
+
"*.swo".to_string(),
70
+
"*~".to_string(),
71
+
".tangled".to_string(),
72
+
".tangled/**".to_string(),
73
+
]
74
+
}
75
+
76
+
/// Load custom ignore patterns from a .wispignore file in the given directory
77
+
fn load_wispignore_file(dir_path: &Path) -> miette::Result<Vec<String>> {
78
+
let wispignore_path = dir_path.join(".wispignore");
79
+
80
+
if !wispignore_path.exists() {
81
+
return Ok(Vec::new());
82
+
}
83
+
84
+
let contents = std::fs::read_to_string(&wispignore_path).into_diagnostic()?;
85
+
86
+
// Parse gitignore-style file (one pattern per line, # for comments)
87
+
let patterns: Vec<String> = contents
88
+
.lines()
89
+
.filter_map(|line| {
90
+
let line = line.trim();
91
+
// Skip empty lines and comments
92
+
if line.is_empty() || line.starts_with('#') {
93
+
None
94
+
} else {
95
+
Some(line.to_string())
96
+
}
97
+
})
98
+
.collect();
99
+
100
+
if !patterns.is_empty() {
101
+
println!("Loaded {} custom patterns from .wispignore", patterns.len());
102
+
}
103
+
104
+
Ok(patterns)
105
+
}
106
+
107
+
/// Build a GlobSet from a list of patterns
108
+
fn build_globset(patterns: Vec<String>) -> miette::Result<GlobSet> {
109
+
let mut builder = GlobSetBuilder::new();
110
+
111
+
for pattern in patterns {
112
+
let glob = Glob::new(&pattern).into_diagnostic()?;
113
+
builder.add(glob);
114
+
}
115
+
116
+
let globset = builder.build().into_diagnostic()?;
117
+
Ok(globset)
118
+
}
119
+
120
+
/// IgnoreMatcher handles checking if paths should be ignored
121
+
pub struct IgnoreMatcher {
122
+
globset: GlobSet,
123
+
}
124
+
125
+
impl IgnoreMatcher {
126
+
/// Create a new IgnoreMatcher for the given directory
127
+
/// Loads default patterns and any custom .wispignore file
128
+
pub fn new(dir_path: &Path) -> miette::Result<Self> {
129
+
let mut all_patterns = load_default_patterns()?;
130
+
131
+
// Load custom patterns from .wispignore
132
+
let custom_patterns = load_wispignore_file(dir_path)?;
133
+
all_patterns.extend(custom_patterns);
134
+
135
+
let globset = build_globset(all_patterns)?;
136
+
137
+
Ok(Self { globset })
138
+
}
139
+
140
+
/// Check if the given path (relative to site root) should be ignored
141
+
pub fn is_ignored(&self, path: &str) -> bool {
142
+
self.globset.is_match(path)
143
+
}
144
+
145
+
/// Check if a filename should be ignored (checks just the filename, not full path)
146
+
pub fn is_filename_ignored(&self, filename: &str) -> bool {
147
+
self.globset.is_match(filename)
148
+
}
149
+
}
+14
-56
cli/src/main.rs
+14
-56
cli/src/main.rs
···
8
8
mod serve;
9
9
mod subfs_utils;
10
10
mod redirects;
11
+
mod ignore_patterns;
11
12
12
13
use clap::{Parser, Subcommand};
13
14
use jacquard::CowStr;
···
323
324
}
324
325
};
325
326
326
-
// Build directory tree
327
-
let (root_dir, total_files, reused_count) = build_directory(agent, &path, &existing_blob_map, String::new()).await?;
327
+
// Build directory tree with ignore patterns
328
+
let ignore_matcher = ignore_patterns::IgnoreMatcher::new(&path)?;
329
+
let (root_dir, total_files, reused_count) = build_directory(agent, &path, &existing_blob_map, String::new(), &ignore_matcher).await?;
328
330
let uploaded_count = total_files - reused_count;
329
331
330
332
// Check if we need to split into subfs records
···
603
605
dir_path: &'a Path,
604
606
existing_blobs: &'a HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>,
605
607
current_path: String,
608
+
ignore_matcher: &'a ignore_patterns::IgnoreMatcher,
606
609
) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<(Directory<'static>, usize, usize)>> + 'a>>
607
610
{
608
611
Box::pin(async move {
···
623
626
.ok_or_else(|| miette::miette!("Invalid filename: {:?}", name))?
624
627
.to_string();
625
628
626
-
// Skip unwanted files and directories
627
-
628
-
// .git directory (version control - thousands of files)
629
-
if name_str == ".git" {
630
-
continue;
631
-
}
632
-
633
-
// .DS_Store (macOS metadata - can leak info)
634
-
if name_str == ".DS_Store" {
635
-
continue;
636
-
}
637
-
638
-
// .wisp.metadata.json (wisp internal metadata - should not be uploaded)
639
-
if name_str == ".wisp.metadata.json" {
640
-
continue;
641
-
}
642
-
643
-
// .env files (environment variables with secrets)
644
-
if name_str.starts_with(".env") {
645
-
continue;
646
-
}
647
-
648
-
// node_modules (dependency folder - can be 100,000+ files)
649
-
if name_str == "node_modules" {
650
-
continue;
651
-
}
652
-
653
-
// OS metadata files
654
-
if name_str == "Thumbs.db" || name_str == "desktop.ini" || name_str.starts_with("._") {
655
-
continue;
656
-
}
629
+
// Construct full path for ignore checking
630
+
let full_path = if current_path.is_empty() {
631
+
name_str.clone()
632
+
} else {
633
+
format!("{}/{}", current_path, name_str)
634
+
};
657
635
658
-
// macOS system directories
659
-
if name_str == ".Spotlight-V100" || name_str == ".Trashes" || name_str == ".fseventsd" {
660
-
continue;
661
-
}
662
-
663
-
// Cache and temp directories
664
-
if name_str == ".cache" || name_str == ".temp" || name_str == ".tmp" {
665
-
continue;
666
-
}
667
-
668
-
// Python cache
669
-
if name_str == "__pycache__" || name_str.ends_with(".pyc") {
670
-
continue;
671
-
}
672
-
673
-
// Python virtual environments
674
-
if name_str == ".venv" || name_str == "venv" || name_str == "env" {
675
-
continue;
676
-
}
677
-
678
-
// Editor swap files
679
-
if name_str.ends_with(".swp") || name_str.ends_with(".swo") || name_str.ends_with("~") {
636
+
// Skip files/directories that match ignore patterns
637
+
if ignore_matcher.is_ignored(&full_path) || ignore_matcher.is_filename_ignored(&name_str) {
680
638
continue;
681
639
}
682
640
···
732
690
} else {
733
691
format!("{}/{}", current_path, name)
734
692
};
735
-
let (subdir, sub_total, sub_reused) = build_directory(agent, &path, existing_blobs, subdir_path).await?;
693
+
let (subdir, sub_total, sub_reused) = build_directory(agent, &path, existing_blobs, subdir_path, ignore_matcher).await?;
736
694
dir_entries.push(Entry::new()
737
695
.name(CowStr::from(name))
738
696
.node(EntryNode::Directory(Box::new(subdir)))