2026-05-20 23:31:58 +07:00
|
|
|
|
// Best-effort sync of per-directory 5chan lists from GitHub.
|
2026-02-15 14:14:21 +08:00
|
|
|
|
// Updates the vendored fallback in src/data/ so production builds ship a fresh snapshot.
|
2026-05-20 23:31:58 +07:00
|
|
|
|
// Never fails the build: if the fetch fails (offline, rate-limited, etc.), the existing file is kept.
|
2026-02-15 14:14:21 +08:00
|
|
|
|
|
2026-05-20 23:31:58 +07:00
|
|
|
|
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
2026-04-13 14:54:23 +07:00
|
|
|
|
import { isAbsolute, join, dirname, resolve } from 'path';
|
2026-02-15 14:14:21 +08:00
|
|
|
|
import { fileURLToPath } from 'url';
|
|
|
|
|
|
|
|
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
|
|
|
|
const __dirname = dirname(__filename);
|
|
|
|
|
|
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const GITHUB_CONTENTS_URL = 'https://api.github.com/repos/bitsocialnet/lists/contents/5chan-directories?ref=master';
|
|
|
|
|
|
const GITHUB_RAW_BASE_URL = 'https://raw.githubusercontent.com/bitsocialnet/lists/master/5chan-directories';
|
2026-04-13 14:54:23 +07:00
|
|
|
|
const DIRECTORIES_SOURCE_PATH = process.env.DIRECTORIES_SOURCE_PATH;
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const OUTPUT_PATH = join(__dirname, '..', 'src', 'data', '5chan-directory-lists.json');
|
2026-02-15 14:14:21 +08:00
|
|
|
|
const TIMEOUT_MS = 5000;
|
2026-02-15 18:27:12 +08:00
|
|
|
|
const DEFAULT_METADATA = {
|
|
|
|
|
|
title: '5chan directories',
|
2026-05-20 23:31:58 +07:00
|
|
|
|
description: 'Directory assignments built from per-directory candidate lists in https://github.com/bitsocialnet/lists/tree/master/5chan-directories',
|
2026-02-15 18:27:12 +08:00
|
|
|
|
createdAt: 0,
|
|
|
|
|
|
updatedAt: 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const DEFAULTS_FILE_NAME = '5chan-directories-defaults.json';
|
|
|
|
|
|
const DIRECTORY_LIST_FILE_RE = /^5chan-.+-directory\.json$/;
|
|
|
|
|
|
const DIRECTORY_CODE_ORDER = [
|
|
|
|
|
|
'a',
|
|
|
|
|
|
'co',
|
|
|
|
|
|
'ck',
|
|
|
|
|
|
'pol',
|
|
|
|
|
|
'biz',
|
|
|
|
|
|
'sci',
|
|
|
|
|
|
'g',
|
|
|
|
|
|
'v',
|
|
|
|
|
|
'vg',
|
|
|
|
|
|
'vr',
|
|
|
|
|
|
'fit',
|
|
|
|
|
|
'sp',
|
|
|
|
|
|
'tg',
|
|
|
|
|
|
'adv',
|
|
|
|
|
|
'wsg',
|
|
|
|
|
|
'diy',
|
|
|
|
|
|
'out',
|
2026-05-30 15:06:47 +07:00
|
|
|
|
'i',
|
2026-05-20 23:31:58 +07:00
|
|
|
|
'ic',
|
|
|
|
|
|
'mu',
|
|
|
|
|
|
'int',
|
|
|
|
|
|
'lit',
|
|
|
|
|
|
'his',
|
|
|
|
|
|
'tv',
|
|
|
|
|
|
't',
|
|
|
|
|
|
'x',
|
|
|
|
|
|
'vip',
|
|
|
|
|
|
'gif',
|
|
|
|
|
|
'bant',
|
|
|
|
|
|
'b',
|
|
|
|
|
|
'an',
|
|
|
|
|
|
];
|
|
|
|
|
|
const DIRECTORY_CODE_ORDER_INDEX = new Map(DIRECTORY_CODE_ORDER.map((code, index) => [code, index]));
|
2026-02-15 18:27:12 +08:00
|
|
|
|
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const isRecord = (value) => typeof value === 'object' && value !== null;
|
|
|
|
|
|
const isDirectoryListFile = (fileName) => DIRECTORY_LIST_FILE_RE.test(fileName);
|
|
|
|
|
|
const toNumber = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : undefined);
|
|
|
|
|
|
const toString = (value) => (typeof value === 'string' && value.length > 0 ? value : undefined);
|
2026-04-13 14:54:23 +07:00
|
|
|
|
|
2026-02-15 18:27:12 +08:00
|
|
|
|
const normalizeFeatures = (value) => {
|
|
|
|
|
|
if (!isRecord(value)) {
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const normalizedFeatures = Object.entries(value).reduce((acc, [key, featureValue]) => {
|
|
|
|
|
|
if (typeof featureValue === 'string' || typeof featureValue === 'boolean' || typeof featureValue === 'number') {
|
|
|
|
|
|
acc[key] = featureValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
return acc;
|
|
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
|
|
|
|
return Object.keys(normalizedFeatures).length > 0 ? normalizedFeatures : undefined;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const deriveNsfw = ({ nsfw, features }) => {
|
2026-04-13 14:54:23 +07:00
|
|
|
|
const safeForWork = typeof features?.safeForWork === 'boolean' ? features.safeForWork : undefined;
|
|
|
|
|
|
if (safeForWork !== undefined) {
|
|
|
|
|
|
return !safeForWork;
|
|
|
|
|
|
}
|
|
|
|
|
|
const featuresNsfw = typeof features?.nsfw === 'boolean' ? features.nsfw : undefined;
|
2026-05-20 23:31:58 +07:00
|
|
|
|
return typeof nsfw === 'boolean' ? nsfw : featuresNsfw;
|
2026-04-13 14:54:23 +07:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const normalizeBoard = (raw) => {
|
|
|
|
|
|
if (!isRecord(raw)) return null;
|
|
|
|
|
|
const address = toString(raw.address) || toString(raw.name);
|
|
|
|
|
|
if (!address) return null;
|
|
|
|
|
|
const features = normalizeFeatures(raw.features);
|
|
|
|
|
|
const nsfw = deriveNsfw({ nsfw: raw.nsfw, features });
|
|
|
|
|
|
const score = toNumber(raw.score);
|
2026-02-15 18:27:12 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
2026-05-20 23:31:58 +07:00
|
|
|
|
address,
|
|
|
|
|
|
...(toString(raw.publicKey) ? { publicKey: toString(raw.publicKey) } : {}),
|
|
|
|
|
|
...(toString(raw.owner) ? { owner: toString(raw.owner) } : {}),
|
|
|
|
|
|
...(score !== undefined ? { score } : {}),
|
|
|
|
|
|
...(toNumber(raw.addedAt) !== undefined ? { addedAt: toNumber(raw.addedAt) } : {}),
|
|
|
|
|
|
...(features ? { features } : {}),
|
|
|
|
|
|
...(nsfw !== undefined ? { nsfw } : {}),
|
2026-02-15 18:27:12 +08:00
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const normalizeDirectoryDefaultsEntry = (code, raw) => {
|
|
|
|
|
|
if (!isRecord(raw)) {
|
|
|
|
|
|
return { directoryCode: code };
|
2026-02-15 18:27:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const features = normalizeFeatures(raw.features);
|
|
|
|
|
|
return {
|
|
|
|
|
|
directoryCode: toString(raw.directoryCode) || code,
|
|
|
|
|
|
...(toString(raw.title) ? { title: toString(raw.title) } : {}),
|
|
|
|
|
|
...(features ? { features } : {}),
|
|
|
|
|
|
};
|
2026-02-15 18:27:12 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const normalizeDirectoryDefaultsData = (raw) => {
|
|
|
|
|
|
const directoriesRaw = isRecord(raw) && isRecord(raw.directories) ? raw.directories : {};
|
|
|
|
|
|
const directories = Object.fromEntries(Object.entries(directoriesRaw).map(([code, value]) => [code, normalizeDirectoryDefaultsEntry(code, value)]));
|
|
|
|
|
|
return {
|
|
|
|
|
|
...(isRecord(raw) && toString(raw.title) ? { title: toString(raw.title) } : {}),
|
|
|
|
|
|
...(isRecord(raw) && toString(raw.description) ? { description: toString(raw.description) } : {}),
|
|
|
|
|
|
...(isRecord(raw) && toNumber(raw.createdAt) !== undefined ? { createdAt: toNumber(raw.createdAt) } : {}),
|
|
|
|
|
|
...(isRecord(raw) && toNumber(raw.updatedAt) !== undefined ? { updatedAt: toNumber(raw.updatedAt) } : {}),
|
|
|
|
|
|
directories,
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const normalizeDirectoryList = (raw, fallbackCode, defaults) => {
|
|
|
|
|
|
if (!isRecord(raw)) return null;
|
|
|
|
|
|
const boardsRaw = Array.isArray(raw.boards) ? raw.boards : Array.isArray(raw.communities) ? raw.communities : null;
|
|
|
|
|
|
if (!boardsRaw) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const boards = boardsRaw.map(normalizeBoard).filter(Boolean);
|
|
|
|
|
|
if (boards.length === 0) return null;
|
|
|
|
|
|
const rawCode = toString(raw.directoryCode);
|
|
|
|
|
|
const defaultEntry = defaults?.directories?.[rawCode || fallbackCode] || defaults?.directories?.[fallbackCode];
|
|
|
|
|
|
const features = normalizeFeatures(defaultEntry?.features) || normalizeFeatures(raw.features);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
directoryCode: toString(defaultEntry?.directoryCode) || rawCode || fallbackCode,
|
|
|
|
|
|
...(toString(defaultEntry?.title) ? { title: toString(defaultEntry.title) } : toString(raw.title) ? { title: toString(raw.title) } : {}),
|
|
|
|
|
|
...(toString(raw.description) ? { description: toString(raw.description) } : {}),
|
|
|
|
|
|
...(features ? { features } : {}),
|
|
|
|
|
|
...(toNumber(raw.createdAt) !== undefined ? { createdAt: toNumber(raw.createdAt) } : {}),
|
|
|
|
|
|
...(toNumber(raw.updatedAt) !== undefined ? { updatedAt: toNumber(raw.updatedAt) } : {}),
|
|
|
|
|
|
boards,
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getCodeFromFileName = (fileName) => fileName.replace(/^5chan-/, '').replace(/-directory\.json$/, '');
|
|
|
|
|
|
|
|
|
|
|
|
const sortDirectoryLists = (lists) =>
|
|
|
|
|
|
[...lists].sort((a, b) => {
|
|
|
|
|
|
const aIndex = DIRECTORY_CODE_ORDER_INDEX.get(a.directoryCode) ?? Number.MAX_SAFE_INTEGER;
|
|
|
|
|
|
const bIndex = DIRECTORY_CODE_ORDER_INDEX.get(b.directoryCode) ?? Number.MAX_SAFE_INTEGER;
|
|
|
|
|
|
if (aIndex !== bIndex) {
|
|
|
|
|
|
return aIndex - bIndex;
|
|
|
|
|
|
}
|
|
|
|
|
|
return a.directoryCode.localeCompare(b.directoryCode);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const toDirectoryListsData = (directories, fallbackMetadata = DEFAULT_METADATA) => {
|
|
|
|
|
|
const timestamps = [fallbackMetadata.createdAt, fallbackMetadata.updatedAt, ...directories.flatMap((list) => [list.createdAt, list.updatedAt])].filter(
|
|
|
|
|
|
(value) => typeof value === 'number',
|
|
|
|
|
|
);
|
|
|
|
|
|
return {
|
|
|
|
|
|
title: fallbackMetadata.title || DEFAULT_METADATA.title,
|
|
|
|
|
|
description: fallbackMetadata.description || DEFAULT_METADATA.description,
|
|
|
|
|
|
createdAt: timestamps.length > 0 ? Math.min(...timestamps) : fallbackMetadata.createdAt,
|
|
|
|
|
|
updatedAt: timestamps.length > 0 ? Math.max(...timestamps) : fallbackMetadata.updatedAt,
|
|
|
|
|
|
directories: sortDirectoryLists(directories),
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const normalizeDirectoryListsData = (value, fallbackMetadata = DEFAULT_METADATA, defaults) => {
|
|
|
|
|
|
if (!isRecord(value) || !Array.isArray(value.directories)) {
|
|
|
|
|
|
return null;
|
2026-02-15 18:27:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const directories = value.directories
|
|
|
|
|
|
.map((directory, index) => {
|
2026-02-15 18:27:12 +08:00
|
|
|
|
if (!isRecord(directory)) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const fallbackCode = toString(directory.directoryCode) || Object.keys(defaults?.directories || {})[index];
|
|
|
|
|
|
return fallbackCode ? normalizeDirectoryList(directory, fallbackCode, defaults) : null;
|
2026-02-15 18:27:12 +08:00
|
|
|
|
})
|
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const metadata = {
|
|
|
|
|
|
title: toString(value.title) || fallbackMetadata.title,
|
|
|
|
|
|
description: toString(value.description) || fallbackMetadata.description,
|
|
|
|
|
|
createdAt: toNumber(value.createdAt) ?? fallbackMetadata.createdAt,
|
|
|
|
|
|
updatedAt: toNumber(value.updatedAt) ?? fallbackMetadata.updatedAt,
|
2026-02-15 18:27:12 +08:00
|
|
|
|
};
|
2026-05-20 23:31:58 +07:00
|
|
|
|
|
|
|
|
|
|
return directories.length > 0 ? toDirectoryListsData(directories, metadata) : null;
|
2026-02-15 18:27:12 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getErrorMessage = (error) => (error instanceof Error ? error.message : String(error));
|
2026-02-15 14:14:21 +08:00
|
|
|
|
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const fetchJson = async (url) => {
|
2026-04-13 14:54:23 +07:00
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
|
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
|
|
|
|
try {
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const response = await fetch(url, { signal: controller.signal });
|
2026-04-13 14:54:23 +07:00
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
return await response.json();
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const loadFromLocalDirectory = (directoryPath) => {
|
|
|
|
|
|
console.log(`ℹ️ Syncing vendored directories from local directory: ${directoryPath}`);
|
|
|
|
|
|
const defaultsPath = join(directoryPath, DEFAULTS_FILE_NAME);
|
|
|
|
|
|
const defaults = existsSync(defaultsPath) ? normalizeDirectoryDefaultsData(JSON.parse(readFileSync(defaultsPath, 'utf8'))) : normalizeDirectoryDefaultsData({});
|
|
|
|
|
|
const directories = readdirSync(directoryPath)
|
|
|
|
|
|
.filter(isDirectoryListFile)
|
|
|
|
|
|
.map((fileName) => {
|
|
|
|
|
|
const raw = JSON.parse(readFileSync(join(directoryPath, fileName), 'utf8'));
|
|
|
|
|
|
return normalizeDirectoryList(raw, getCodeFromFileName(fileName), defaults);
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
|
|
|
|
return toDirectoryListsData(directories, {
|
|
|
|
|
|
title: DEFAULT_METADATA.title,
|
|
|
|
|
|
description: DEFAULT_METADATA.description,
|
|
|
|
|
|
createdAt: defaults.createdAt ?? DEFAULT_METADATA.createdAt,
|
|
|
|
|
|
updatedAt: defaults.updatedAt ?? DEFAULT_METADATA.updatedAt,
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const loadDirectoriesSource = async () => {
|
|
|
|
|
|
if (DIRECTORIES_SOURCE_PATH) {
|
|
|
|
|
|
const resolvedSourcePath = isAbsolute(DIRECTORIES_SOURCE_PATH) ? DIRECTORIES_SOURCE_PATH : resolve(process.cwd(), DIRECTORIES_SOURCE_PATH);
|
|
|
|
|
|
if (!existsSync(resolvedSourcePath)) {
|
|
|
|
|
|
throw new Error(`Local directories source not found: ${resolvedSourcePath}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (statSync(resolvedSourcePath).isDirectory()) {
|
|
|
|
|
|
return loadFromLocalDirectory(resolvedSourcePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`ℹ️ Syncing vendored directories from local file: ${resolvedSourcePath}`);
|
|
|
|
|
|
return JSON.parse(readFileSync(resolvedSourcePath, 'utf8'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`ℹ️ Syncing vendored directories from GitHub folder: ${GITHUB_CONTENTS_URL}`);
|
|
|
|
|
|
const contents = await fetchJson(GITHUB_CONTENTS_URL);
|
|
|
|
|
|
if (!Array.isArray(contents)) {
|
|
|
|
|
|
throw new Error('Invalid GitHub directory listing');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-30 15:06:47 +07:00
|
|
|
|
const fileNames = contents
|
|
|
|
|
|
.map((entry) => (isRecord(entry) ? entry.name : undefined))
|
|
|
|
|
|
.filter((name) => typeof name === 'string' && isDirectoryListFile(name))
|
|
|
|
|
|
.sort();
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const defaults = normalizeDirectoryDefaultsData(await fetchJson(`${GITHUB_RAW_BASE_URL}/${DEFAULTS_FILE_NAME}`));
|
|
|
|
|
|
const directories = await Promise.all(
|
|
|
|
|
|
fileNames.map(async (fileName) => {
|
|
|
|
|
|
const raw = await fetchJson(`${GITHUB_RAW_BASE_URL}/${fileName}`);
|
|
|
|
|
|
return normalizeDirectoryList(raw, getCodeFromFileName(fileName), defaults);
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return toDirectoryListsData(directories.filter(Boolean), {
|
|
|
|
|
|
title: DEFAULT_METADATA.title,
|
|
|
|
|
|
description: DEFAULT_METADATA.description,
|
|
|
|
|
|
createdAt: defaults.createdAt ?? DEFAULT_METADATA.createdAt,
|
|
|
|
|
|
updatedAt: defaults.updatedAt ?? DEFAULT_METADATA.updatedAt,
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-15 14:14:21 +08:00
|
|
|
|
const sync = async () => {
|
|
|
|
|
|
try {
|
2026-02-15 18:27:12 +08:00
|
|
|
|
let existing = '';
|
|
|
|
|
|
let fallbackMetadata = DEFAULT_METADATA;
|
|
|
|
|
|
try {
|
|
|
|
|
|
existing = readFileSync(OUTPUT_PATH, 'utf8');
|
|
|
|
|
|
const parsedExisting = JSON.parse(existing);
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const normalizedExisting = normalizeDirectoryListsData(parsedExisting);
|
2026-02-15 18:27:12 +08:00
|
|
|
|
if (normalizedExisting) {
|
|
|
|
|
|
fallbackMetadata = {
|
|
|
|
|
|
title: normalizedExisting.title,
|
|
|
|
|
|
description: normalizedExisting.description,
|
|
|
|
|
|
createdAt: normalizedExisting.createdAt,
|
|
|
|
|
|
updatedAt: normalizedExisting.updatedAt,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
2026-05-20 23:31:58 +07:00
|
|
|
|
// File does not exist yet or is invalid JSON.
|
2026-02-15 14:14:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const data = normalizeDirectoryListsData(await loadDirectoriesSource(), fallbackMetadata);
|
2026-02-15 18:27:12 +08:00
|
|
|
|
if (!data) {
|
2026-05-20 23:31:58 +07:00
|
|
|
|
throw new Error('Invalid directory lists payload');
|
2026-02-15 14:14:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 23:31:58 +07:00
|
|
|
|
const formatted = `${JSON.stringify(data, null, 2)}\n`;
|
2026-02-15 14:14:21 +08:00
|
|
|
|
|
|
|
|
|
|
if (formatted === existing) {
|
2026-05-20 23:31:58 +07:00
|
|
|
|
console.log('✅ Vendored directory lists already up to date');
|
2026-02-15 14:14:21 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
writeFileSync(OUTPUT_PATH, formatted, 'utf8');
|
2026-05-20 23:31:58 +07:00
|
|
|
|
console.log(`✅ Synced vendored directory lists (${data.directories.length} directories)`);
|
2026-02-15 14:14:21 +08:00
|
|
|
|
} catch (e) {
|
2026-05-20 23:31:58 +07:00
|
|
|
|
console.warn(`⚠️ Could not sync directory lists from GitHub (keeping existing file): ${getErrorMessage(e)}`);
|
2026-02-15 14:14:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
sync();
|