@consilioweb/spellcheck 0.10.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.
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Post-install setup for @consilioweb/spellcheck
5
+ * Automatically adds the plugin to the Payload config.
6
+ *
7
+ * Usage: npx spellcheck-install
8
+ * or: npx spellcheck-install --collections pages,posts --language fr
9
+ */
10
+
11
+ import fs from 'node:fs'
12
+ import path from 'node:path'
13
+ import { execSync } from 'node:child_process'
14
+
15
+ const PACKAGE_NAME = '@consilioweb/spellcheck'
16
+
17
+ // ── Helpers ──────────────────────────────────────────────
18
+
19
+ function detectPackageManager(dir) {
20
+ if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm'
21
+ if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn'
22
+ if (fs.existsSync(path.join(dir, 'bun.lockb')) || fs.existsSync(path.join(dir, 'bun.lock'))) return 'bun'
23
+ return 'npm'
24
+ }
25
+
26
+ function run(cmd, cwd) {
27
+ console.log(` \x1b[90m$ ${cmd}\x1b[0m`)
28
+ try {
29
+ execSync(cmd, { cwd, stdio: 'inherit' })
30
+ return true
31
+ } catch {
32
+ return false
33
+ }
34
+ }
35
+
36
+ function findSourceFiles(dir) {
37
+ const results = []
38
+ if (!fs.existsSync(dir)) return results
39
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
40
+ for (const entry of entries) {
41
+ const fullPath = path.join(dir, entry.name)
42
+ if (entry.isDirectory()) {
43
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
44
+ results.push(...findSourceFiles(fullPath))
45
+ } else if (/\.(ts|tsx)$/.test(entry.name)) {
46
+ results.push(fullPath)
47
+ }
48
+ }
49
+ return results
50
+ }
51
+
52
+ /**
53
+ * Find the plugins file (src/plugins/index.ts or similar)
54
+ */
55
+ function findPluginsFile(srcDir) {
56
+ // Common locations
57
+ const candidates = [
58
+ path.join(srcDir, 'plugins', 'index.ts'),
59
+ path.join(srcDir, 'plugins.ts'),
60
+ ]
61
+
62
+ for (const candidate of candidates) {
63
+ if (fs.existsSync(candidate)) return candidate
64
+ }
65
+
66
+ // Search for a file containing `Plugin[]` or `plugins:`
67
+ const files = findSourceFiles(srcDir)
68
+ for (const file of files) {
69
+ const content = fs.readFileSync(file, 'utf-8')
70
+ if (content.includes('export const plugins') && content.includes('Plugin[]')) {
71
+ return file
72
+ }
73
+ }
74
+
75
+ return null
76
+ }
77
+
78
+ /**
79
+ * Check if the plugin is already imported/used
80
+ */
81
+ function isAlreadyInstalled(content) {
82
+ return content.includes(PACKAGE_NAME)
83
+ }
84
+
85
+ /**
86
+ * Parse CLI args for --collections and --language
87
+ */
88
+ function parseArgs() {
89
+ const args = process.argv.slice(2)
90
+ const config = {
91
+ collections: ['pages', 'posts'],
92
+ language: 'fr',
93
+ }
94
+
95
+ for (let i = 0; i < args.length; i++) {
96
+ if (args[i] === '--collections' && args[i + 1]) {
97
+ config.collections = args[++i].split(',').map(s => s.trim())
98
+ }
99
+ if (args[i] === '--language' && args[i + 1]) {
100
+ config.language = args[++i].trim()
101
+ }
102
+ }
103
+
104
+ return config
105
+ }
106
+
107
+ // ── Main ──────────────────────────────────────────────
108
+
109
+ function main() {
110
+ const projectDir = process.env.INIT_CWD || process.cwd()
111
+ const srcDir = path.join(projectDir, 'src')
112
+ const pm = detectPackageManager(projectDir)
113
+ const config = parseArgs()
114
+
115
+ console.log('')
116
+ console.log(' \x1b[36m@consilioweb/spellcheck\x1b[0m — Install')
117
+ console.log(' ─────────────────────────────────────────────')
118
+ console.log(` Project: \x1b[33m${projectDir}\x1b[0m`)
119
+ console.log(` Package manager: \x1b[33m${pm}\x1b[0m`)
120
+ console.log(` Collections: \x1b[33m${config.collections.join(', ')}\x1b[0m`)
121
+ console.log(` Language: \x1b[33m${config.language}\x1b[0m`)
122
+ console.log('')
123
+
124
+ // ── Step 1: Find plugins file ──
125
+ console.log(' \x1b[36m[1/3]\x1b[0m Finding plugins configuration...')
126
+
127
+ const pluginsFile = findPluginsFile(srcDir)
128
+ if (!pluginsFile) {
129
+ console.log(' \x1b[33m⚠\x1b[0m No plugins file found in src/.')
130
+ console.log(' \x1b[33m⚠\x1b[0m You need to manually add the plugin to your Payload config:')
131
+ console.log('')
132
+ console.log(` \x1b[90mimport { spellcheckPlugin } from '${PACKAGE_NAME}'\x1b[0m`)
133
+ console.log(` \x1b[90mspellcheckPlugin({ collections: ${JSON.stringify(config.collections)}, language: '${config.language}' })\x1b[0m`)
134
+ console.log('')
135
+ } else {
136
+ const relPath = path.relative(projectDir, pluginsFile)
137
+ console.log(` \x1b[32m✓\x1b[0m Found: ${relPath}`)
138
+
139
+ const content = fs.readFileSync(pluginsFile, 'utf-8')
140
+
141
+ if (isAlreadyInstalled(content)) {
142
+ console.log(` \x1b[32m✓\x1b[0m Plugin already configured — skipping.`)
143
+ } else {
144
+ // Add import at the top (after last import line)
145
+ const importLine = `import { spellcheckPlugin } from '${PACKAGE_NAME}'`
146
+ const lines = content.split('\n')
147
+ let lastImportIndex = -1
148
+
149
+ for (let i = 0; i < lines.length; i++) {
150
+ if (lines[i].trim().startsWith('import ')) {
151
+ lastImportIndex = i
152
+ }
153
+ }
154
+
155
+ if (lastImportIndex >= 0) {
156
+ lines.splice(lastImportIndex + 1, 0, importLine)
157
+ } else {
158
+ lines.unshift(importLine)
159
+ }
160
+
161
+ // Add plugin call before the closing bracket of the plugins array
162
+ let joined = lines.join('\n')
163
+
164
+ // Build plugin config string
165
+ const collectionsStr = config.collections.map(c => `'${c}'`).join(', ')
166
+ const pluginCall = ` spellcheckPlugin({
167
+ collections: [${collectionsStr}],
168
+ language: '${config.language}',
169
+ checkOnSave: true,
170
+ addSidebarField: true,
171
+ addDashboardView: true,
172
+ skipRules: ['FR_SPELLING_RULE', 'WHITESPACE_RULE'],
173
+ skipCategories: ['TYPOGRAPHY', 'STYLE'],
174
+ customDictionary: ['Next.js', 'Payload', 'TypeScript', 'SEO'],
175
+ }),`
176
+
177
+ // Find the plugins array closing bracket and insert before it
178
+ const pluginsArrayMatch = joined.match(/export\s+const\s+plugins\s*:\s*Plugin\[\]\s*=\s*\[/)
179
+ if (pluginsArrayMatch) {
180
+ // Find the last `]` that closes this array
181
+ const arrayStart = joined.indexOf(pluginsArrayMatch[0])
182
+ const afterStart = joined.indexOf('[', arrayStart + pluginsArrayMatch[0].length - 1)
183
+
184
+ // Find matching closing bracket
185
+ let depth = 0
186
+ let closingIdx = -1
187
+ for (let i = afterStart; i < joined.length; i++) {
188
+ if (joined[i] === '[') depth++
189
+ else if (joined[i] === ']') {
190
+ depth--
191
+ if (depth === 0) {
192
+ closingIdx = i
193
+ break
194
+ }
195
+ }
196
+ }
197
+
198
+ if (closingIdx > 0) {
199
+ joined = joined.slice(0, closingIdx) + pluginCall + '\n' + joined.slice(closingIdx)
200
+ }
201
+ }
202
+
203
+ fs.writeFileSync(pluginsFile, joined, 'utf-8')
204
+ console.log(` \x1b[32m✓\x1b[0m Plugin added to ${relPath}`)
205
+ }
206
+ }
207
+
208
+ console.log('')
209
+
210
+ // ── Step 2: Regenerate importmap ──
211
+ console.log(' \x1b[36m[2/3]\x1b[0m Regenerating importmap...')
212
+ const importmapCmd = pm === 'npm' ? 'npx payload' : `${pm} payload`
213
+ run(`${importmapCmd} generate:importmap`, projectDir)
214
+
215
+ console.log('')
216
+
217
+ // ── Step 3: Summary ──
218
+ console.log(' \x1b[36m[3/3]\x1b[0m Post-install checks...')
219
+ console.log(' \x1b[32m✓\x1b[0m Collection \x1b[33mspellcheck-results\x1b[0m will be auto-created on first boot')
220
+ console.log(' \x1b[32m✓\x1b[0m Endpoints registered: /api/spellcheck/validate, /api/spellcheck/fix, /api/spellcheck/bulk')
221
+ console.log(' \x1b[32m✓\x1b[0m Sidebar field added to editor')
222
+ console.log(' \x1b[32m✓\x1b[0m Dashboard view at /admin/spellcheck')
223
+
224
+ console.log('')
225
+ console.log(' \x1b[32m✓ Install complete!\x1b[0m')
226
+ console.log('')
227
+ console.log(' \x1b[36mNext steps:\x1b[0m')
228
+ console.log(' 1. Start your dev server to create the DB table')
229
+ console.log(' 2. Visit /admin/spellcheck to scan your content')
230
+ console.log(' 3. (Optional) Add "Correcteur" to your admin nav')
231
+ console.log('')
232
+ console.log(' \x1b[36mTo uninstall:\x1b[0m npx spellcheck-uninstall')
233
+ console.log('')
234
+ }
235
+
236
+ main()
@@ -0,0 +1,350 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Full uninstall for @consilioweb/spellcheck
5
+ * Removes all imports, plugin calls, DB tables, and the package itself.
6
+ *
7
+ * Usage: npx spellcheck-uninstall
8
+ * or: npx spellcheck-uninstall --keep-data (skip DB cleanup)
9
+ */
10
+
11
+ import fs from 'node:fs'
12
+ import path from 'node:path'
13
+ import { execSync } from 'node:child_process'
14
+
15
+ const PACKAGE_NAME = '@consilioweb/spellcheck'
16
+
17
+ // Tables and indexes created by the plugin
18
+ const DB_TABLES = ['spellcheck_results', 'spellcheck_dictionary']
19
+ const DB_INDEXES = [
20
+ 'spellcheck_results_doc_id_idx',
21
+ 'spellcheck_results_collection_idx',
22
+ 'spellcheck_results_last_checked_idx',
23
+ 'spellcheck_dictionary_word_idx',
24
+ ]
25
+
26
+ // Regex to match any import line from @consilioweb/spellcheck
27
+ const IMPORT_RE = /^\s*import\s+(?:type\s+)?(?:\{[^}]*\}|[\w]+)\s+from\s+['"]@consilioweb\/spellcheck(?:\/[^'"]*)?['"]\s*;?\s*$/gm
28
+
29
+ // ── Helpers ──────────────────────────────────────────────
30
+
31
+ function detectPackageManager(dir) {
32
+ if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm'
33
+ if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn'
34
+ if (fs.existsSync(path.join(dir, 'bun.lockb')) || fs.existsSync(path.join(dir, 'bun.lock'))) return 'bun'
35
+ return 'npm'
36
+ }
37
+
38
+ function run(cmd, cwd) {
39
+ console.log(` \x1b[90m$ ${cmd}\x1b[0m`)
40
+ try {
41
+ execSync(cmd, { cwd, stdio: 'inherit' })
42
+ return true
43
+ } catch {
44
+ return false
45
+ }
46
+ }
47
+
48
+ function runSilent(cmd, cwd) {
49
+ try {
50
+ return execSync(cmd, { cwd, encoding: 'utf-8' }).trim()
51
+ } catch {
52
+ return ''
53
+ }
54
+ }
55
+
56
+ function findSourceFiles(dir) {
57
+ const results = []
58
+ if (!fs.existsSync(dir)) return results
59
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
60
+ for (const entry of entries) {
61
+ const fullPath = path.join(dir, entry.name)
62
+ if (entry.isDirectory()) {
63
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
64
+ results.push(...findSourceFiles(fullPath))
65
+ } else if (/\.(ts|tsx)$/.test(entry.name)) {
66
+ results.push(fullPath)
67
+ }
68
+ }
69
+ return results
70
+ }
71
+
72
+ /**
73
+ * Extract imported names from @consilioweb/spellcheck imports
74
+ */
75
+ function extractImportedNames(content) {
76
+ const names = []
77
+ const re = /^\s*import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+['"]@consilioweb\/spellcheck(?:\/[^'"]*)?['"]\s*;?\s*$/gm
78
+ let match
79
+ while ((match = re.exec(content)) !== null) {
80
+ for (const spec of match[1].split(',')) {
81
+ const trimmed = spec.trim()
82
+ if (!trimmed) continue
83
+ const asParts = trimmed.split(/\s+as\s+/)
84
+ names.push(asParts.length > 1 ? asParts[1].trim() : trimmed)
85
+ }
86
+ }
87
+ return names
88
+ }
89
+
90
+ /**
91
+ * Remove plugin calls (handles nested braces/parens across multiple lines)
92
+ */
93
+ function removePluginCalls(content, callNames) {
94
+ let modified = content
95
+
96
+ for (const fnName of callNames) {
97
+ let searchFrom = 0
98
+ while (true) {
99
+ const callIndex = modified.indexOf(`${fnName}(`, searchFrom)
100
+ if (callIndex === -1) break
101
+
102
+ // Verify not part of a larger identifier
103
+ if (callIndex > 0 && /[\w$]/.test(modified[callIndex - 1])) {
104
+ searchFrom = callIndex + fnName.length
105
+ continue
106
+ }
107
+
108
+ // Find line start
109
+ let lineStart = callIndex
110
+ while (lineStart > 0 && modified[lineStart - 1] !== '\n') lineStart--
111
+
112
+ // Find matching closing paren
113
+ const openParen = callIndex + fnName.length
114
+ let depth = 0
115
+ let endIndex = openParen
116
+ for (let i = openParen; i < modified.length; i++) {
117
+ if (modified[i] === '(') depth++
118
+ else if (modified[i] === ')') {
119
+ depth--
120
+ if (depth === 0) {
121
+ endIndex = i + 1
122
+ break
123
+ }
124
+ }
125
+ }
126
+
127
+ // Handle trailing comma
128
+ let removeEnd = endIndex
129
+ const afterCall = modified.slice(endIndex)
130
+ const trailingMatch = afterCall.match(/^\s*,/)
131
+ if (trailingMatch) {
132
+ removeEnd = endIndex + trailingMatch[0].length
133
+ }
134
+
135
+ // Determine full range
136
+ let removeStart = lineStart
137
+ if (removeStart > 0 && modified[removeStart - 1] === '\n') removeStart--
138
+
139
+ // If no trailing comma, try removing leading comma
140
+ if (!trailingMatch) {
141
+ let lookBack = removeStart
142
+ while (lookBack > 0 && /[\s\n]/.test(modified[lookBack - 1])) lookBack--
143
+ if (lookBack > 0 && modified[lookBack - 1] === ',') removeStart = lookBack - 1
144
+ }
145
+
146
+ modified = modified.slice(0, removeStart) + modified.slice(removeEnd)
147
+ }
148
+ }
149
+
150
+ return modified
151
+ }
152
+
153
+ function cleanEmptyLines(content) {
154
+ return content.replace(/\n{3,}/g, '\n\n')
155
+ }
156
+
157
+ function cleanOrphanCommas(content) {
158
+ return content.replace(/,(\s*\n\s*[)\]])/g, '$1')
159
+ }
160
+
161
+ /**
162
+ * Process a single source file: remove imports and plugin calls
163
+ */
164
+ function processFile(filePath) {
165
+ const original = fs.readFileSync(filePath, 'utf-8')
166
+ if (!original.includes(PACKAGE_NAME)) return null
167
+
168
+ let content = original
169
+
170
+ // Extract names before removing imports
171
+ const importedNames = extractImportedNames(content)
172
+
173
+ // Remove import lines
174
+ content = content.replace(IMPORT_RE, '')
175
+
176
+ // Remove plugin calls
177
+ if (importedNames.length > 0) {
178
+ content = removePluginCalls(content, importedNames)
179
+ }
180
+
181
+ // Also remove any admin-nav items referencing /admin/spellcheck
182
+ content = content.replace(/\s*\{[^}]*href:\s*['"]\/admin\/spellcheck['"][^}]*\},?\s*/g, '')
183
+
184
+ // Clean up
185
+ content = cleanOrphanCommas(content)
186
+ content = cleanEmptyLines(content)
187
+
188
+ return content === original ? null : content
189
+ }
190
+
191
+ /**
192
+ * Find SQLite DB files in the project
193
+ */
194
+ function findDatabaseFiles(projectDir) {
195
+ const dbFiles = []
196
+ const entries = fs.readdirSync(projectDir, { withFileTypes: true })
197
+ for (const entry of entries) {
198
+ if (entry.isFile() && entry.name.endsWith('.db')) {
199
+ dbFiles.push(path.join(projectDir, entry.name))
200
+ }
201
+ }
202
+ // Also check data/ subdirectory
203
+ const dataDir = path.join(projectDir, 'data')
204
+ if (fs.existsSync(dataDir)) {
205
+ const dataEntries = fs.readdirSync(dataDir, { withFileTypes: true })
206
+ for (const entry of dataEntries) {
207
+ if (entry.isFile() && entry.name.endsWith('.db')) {
208
+ dbFiles.push(path.join(dataDir, entry.name))
209
+ }
210
+ }
211
+ }
212
+ return dbFiles
213
+ }
214
+
215
+ /**
216
+ * Drop spellcheck tables and indexes from a SQLite database
217
+ */
218
+ function cleanDatabase(dbPath) {
219
+ const statements = []
220
+
221
+ // Drop indexes first
222
+ for (const idx of DB_INDEXES) {
223
+ statements.push(`DROP INDEX IF EXISTS \`${idx}\`;`)
224
+ }
225
+
226
+ // Drop tables
227
+ for (const table of DB_TABLES) {
228
+ statements.push(`DROP TABLE IF EXISTS \`${table}\`;`)
229
+ }
230
+
231
+ // Also clean up payload_locked_documents_rels
232
+ statements.push(
233
+ `DELETE FROM \`payload_locked_documents_rels\` WHERE \`spellcheck_results_id\` IS NOT NULL;`,
234
+ `DELETE FROM \`payload_locked_documents_rels\` WHERE \`spellcheck_dictionary_id\` IS NOT NULL;`,
235
+ )
236
+
237
+ // Remove the column from payload_locked_documents_rels (SQLite doesn't support DROP COLUMN easily)
238
+ // Just log it as manual step
239
+
240
+ const sql = statements.join('\n')
241
+ const result = runSilent(`sqlite3 "${dbPath}" "${sql}"`, path.dirname(dbPath))
242
+ return result !== undefined
243
+ }
244
+
245
+ // ── Main ──────────────────────────────────────────────
246
+
247
+ function main() {
248
+ const projectDir = process.env.INIT_CWD || process.cwd()
249
+ const srcDir = path.join(projectDir, 'src')
250
+ const pm = detectPackageManager(projectDir)
251
+ const keepData = process.argv.includes('--keep-data')
252
+
253
+ console.log('')
254
+ console.log(' \x1b[36m@consilioweb/spellcheck\x1b[0m — Full Uninstall')
255
+ console.log(' ─────────────────────────────────────────────')
256
+ console.log(` Project: \x1b[33m${projectDir}\x1b[0m`)
257
+ console.log(` Package manager: \x1b[33m${pm}\x1b[0m`)
258
+ if (keepData) console.log(` \x1b[33m--keep-data: DB tables will NOT be dropped\x1b[0m`)
259
+ console.log('')
260
+
261
+ // ── Step 1: Clean source files ──
262
+ console.log(' \x1b[36m[1/4]\x1b[0m Cleaning source files...')
263
+
264
+ if (!fs.existsSync(srcDir)) {
265
+ console.log(' \x1b[33m⚠\x1b[0m No src/ directory found. Skipping code cleanup.')
266
+ } else {
267
+ const files = findSourceFiles(srcDir)
268
+ const modified = []
269
+
270
+ for (const filePath of files) {
271
+ const result = processFile(filePath)
272
+ if (result !== null) {
273
+ fs.writeFileSync(filePath, result, 'utf-8')
274
+ const rel = path.relative(projectDir, filePath)
275
+ modified.push(rel)
276
+ console.log(` \x1b[32m✓\x1b[0m Cleaned: ${rel}`)
277
+ }
278
+ }
279
+
280
+ if (modified.length === 0) {
281
+ console.log(' \x1b[32m✓\x1b[0m No references found in source files.')
282
+ } else {
283
+ console.log(` \x1b[32m✓\x1b[0m ${modified.length} file(s) cleaned.`)
284
+ }
285
+ }
286
+
287
+ console.log('')
288
+
289
+ // ── Step 2: Clean database ──
290
+ if (!keepData) {
291
+ console.log(' \x1b[36m[2/4]\x1b[0m Cleaning database...')
292
+
293
+ const dbFiles = findDatabaseFiles(projectDir)
294
+ if (dbFiles.length === 0) {
295
+ console.log(' \x1b[33m⚠\x1b[0m No .db files found. Skipping DB cleanup.')
296
+ } else {
297
+ for (const dbFile of dbFiles) {
298
+ const rel = path.relative(projectDir, dbFile)
299
+ const success = cleanDatabase(dbFile)
300
+ if (success) {
301
+ console.log(` \x1b[32m✓\x1b[0m Cleaned: ${rel}`)
302
+ for (const table of DB_TABLES) {
303
+ console.log(` \x1b[90m- Dropped table: ${table}\x1b[0m`)
304
+ }
305
+ for (const idx of DB_INDEXES) {
306
+ console.log(` \x1b[90m- Dropped index: ${idx}\x1b[0m`)
307
+ }
308
+ } else {
309
+ console.log(` \x1b[33m⚠\x1b[0m Could not clean ${rel} (sqlite3 not found or DB locked)`)
310
+ }
311
+ }
312
+ }
313
+ } else {
314
+ console.log(' \x1b[36m[2/4]\x1b[0m Skipping database cleanup (--keep-data)')
315
+ }
316
+
317
+ console.log('')
318
+
319
+ // ── Step 3: Remove the package ──
320
+ console.log(' \x1b[36m[3/4]\x1b[0m Removing package...')
321
+ const removeCmd = pm === 'npm' ? 'npm uninstall' : `${pm} remove`
322
+ run(`${removeCmd} ${PACKAGE_NAME}`, projectDir)
323
+
324
+ console.log('')
325
+
326
+ // ── Step 4: Regenerate importmap ──
327
+ console.log(' \x1b[36m[4/4]\x1b[0m Regenerating importmap...')
328
+ const importmapCmd = pm === 'npm' ? 'npx payload' : `${pm} payload`
329
+ run(`${importmapCmd} generate:importmap`, projectDir)
330
+
331
+ console.log('')
332
+
333
+ // ── Done ──
334
+ console.log(' \x1b[32m✓ Uninstall complete!\x1b[0m')
335
+ console.log('')
336
+
337
+ if (keepData) {
338
+ console.log(' \x1b[36mNote:\x1b[0m Database tables were preserved (--keep-data).')
339
+ console.log(' To manually drop them:')
340
+ console.log(' \x1b[90m sqlite3 your.db "DROP TABLE IF EXISTS spellcheck_results; DROP TABLE IF EXISTS spellcheck_dictionary;"\x1b[0m')
341
+ } else {
342
+ console.log(' \x1b[36mNote:\x1b[0m If columns \x1b[33mspellcheck_results_id\x1b[0m or \x1b[33mspellcheck_dictionary_id\x1b[0m')
343
+ console.log(' remain in \x1b[33mpayload_locked_documents_rels\x1b[0m, they will be ignored by Payload.')
344
+ console.log(' SQLite does not support DROP COLUMN natively.')
345
+ }
346
+
347
+ console.log('')
348
+ }
349
+
350
+ main()