magic-builder 0.1.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.
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+ const { success, fail } = require('../lib/output');
8
+
9
+ const SKILL_ID = 'magic-builder';
10
+ const ZIP_URL = 'https://magic-builder.tos-cn-beijing.volces.com/skills/magic-builder.skill.zip';
11
+ const REPLACED_SKILLS = [
12
+ 'generate-magic-page',
13
+ 'generate-magic-doc',
14
+ 'publish-magic-page',
15
+ 'publish-magic-faas',
16
+ 'magic-url-preview',
17
+ 'upload-file-to-tos',
18
+ 'magic-user-feedback',
19
+ 'check-magic-builder-update',
20
+ ];
21
+
22
+ async function run(args, opts) {
23
+ const sub = args[0];
24
+ if (sub !== 'check-update' && sub !== 'update' && sub !== 'install') {
25
+ fail('Usage: magic-builder skill <check-update|update|install>', 'E_INVALID_ARGS');
26
+ }
27
+ const skillsRoot = path.resolve(opts.skillsRoot || path.join(os.homedir(), '.codex', 'skills'));
28
+ const environment = resolveEnvironment(String(opts.environment || 'auto'), skillsRoot);
29
+ const remote = await prepareRemotePackage();
30
+ const report = buildReport(remote, skillsRoot, environment);
31
+ if (environment === 'cloud') report.cloud_update = buildCloudUpdateDescriptor(report, { includeSkill: sub === 'install' });
32
+
33
+ if (sub === 'update' || sub === 'install') {
34
+ if (environment === 'cloud') {
35
+ success(report, opts);
36
+ return;
37
+ }
38
+ report.install = await installUpdate(report, remote, skillsRoot);
39
+ if (sub === 'install') report.installed = true;
40
+ }
41
+ success(report, opts);
42
+ }
43
+
44
+ async function downloadFile(url, dest) {
45
+ const res = await fetch(url, { redirect: 'follow' });
46
+ if (!res.ok) throw new Error(`GET ${url} failed: HTTP ${res.status}`);
47
+ const arrayBuffer = await res.arrayBuffer();
48
+ fs.writeFileSync(dest, Buffer.from(arrayBuffer));
49
+ }
50
+
51
+ async function prepareRemotePackage() {
52
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
53
+ const workDir = fs.mkdtempSync(path.join(os.tmpdir(), `magic-builder-remote-${stamp}-`));
54
+ const zipPath = path.join(workDir, 'magic-builder.skill.zip');
55
+ const extractDir = path.join(workDir, 'extract');
56
+ fs.mkdirSync(extractDir, { recursive: true });
57
+ await downloadFile(ZIP_URL, zipPath);
58
+ runUnzip(zipPath, extractDir);
59
+
60
+ const candidates = [
61
+ path.join(extractDir, SKILL_ID),
62
+ path.join(extractDir, SKILL_ID, SKILL_ID),
63
+ ];
64
+ const packageRoot = candidates.find((candidate) => fs.existsSync(path.join(candidate, 'SKILL.md')));
65
+ if (!packageRoot) throw new Error(`Downloaded zip does not contain ${SKILL_ID}/SKILL.md`);
66
+ const meta = readSkillMetadata(packageRoot);
67
+ if (meta.name !== SKILL_ID || !meta.version) {
68
+ throw new Error(`Invalid remote SKILL.md metadata: name=${meta.name || ''}, version=${meta.version || ''}`);
69
+ }
70
+ return { workDir, zipPath, extractDir, packageRoot, version: meta.version };
71
+ }
72
+
73
+ function buildReport(remote, skillsRoot, environment) {
74
+ const local = readLocalVersion(skillsRoot);
75
+ const cmp = local.exists && local.version ? compareVersions(remote.version, local.version) : 1;
76
+ const status = !local.exists ? 'missing' : cmp > 0 ? 'update_available' : cmp < 0 ? 'local_newer' : 'current';
77
+ return {
78
+ ok: true,
79
+ environment,
80
+ update_strategy: environment === 'cloud' ? 'cloud_managed_skill_update' : 'local_filesystem',
81
+ package: { name: SKILL_ID, remote_version: remote.version, zip_url: ZIP_URL },
82
+ skills_root: skillsRoot,
83
+ update_available: status === 'missing' || status === 'update_available',
84
+ skill: {
85
+ id: SKILL_ID,
86
+ local_version: local.version,
87
+ remote_version: remote.version,
88
+ local_path: local.path,
89
+ status,
90
+ replaces: REPLACED_SKILLS,
91
+ },
92
+ };
93
+ }
94
+
95
+ function buildCloudUpdateDescriptor(report, options = {}) {
96
+ const includeSkill = options.includeSkill || report.update_available;
97
+ return {
98
+ strategy: 'cloud_managed_skill_update',
99
+ package: report.package,
100
+ skills: includeSkill ? [{
101
+ id: SKILL_ID,
102
+ local_version: report.skill.local_version,
103
+ remote_version: report.skill.remote_version,
104
+ status: report.skill.status,
105
+ replaces: REPLACED_SKILLS,
106
+ }] : [],
107
+ instructions: [
108
+ 'Use the cloud runtime managed skill update/install mechanism.',
109
+ 'Do not copy files into ~/.codex/skills or overwrite local filesystem paths in cloud mode.',
110
+ 'If no managed cloud updater is exposed, report this descriptor and do not claim the update was applied.',
111
+ ],
112
+ };
113
+ }
114
+
115
+ async function installUpdate(report, remote, skillsRoot) {
116
+ const backupDir = path.join(remote.workDir, 'backup');
117
+ fs.mkdirSync(backupDir, { recursive: true });
118
+ const installed = [];
119
+ const removed = [];
120
+ const targetDir = path.join(skillsRoot, SKILL_ID);
121
+ if (fs.existsSync(targetDir)) {
122
+ fs.cpSync(targetDir, path.join(backupDir, SKILL_ID), { recursive: true });
123
+ fs.rmSync(targetDir, { recursive: true, force: true });
124
+ }
125
+ fs.mkdirSync(skillsRoot, { recursive: true });
126
+ fs.cpSync(remote.packageRoot, targetDir, { recursive: true });
127
+ installed.push(SKILL_ID);
128
+
129
+ for (const replacedId of REPLACED_SKILLS) {
130
+ ensureSafeSkillId(replacedId);
131
+ const replacedDir = path.join(skillsRoot, replacedId);
132
+ if (!fs.existsSync(replacedDir)) continue;
133
+ fs.cpSync(replacedDir, path.join(backupDir, replacedId), { recursive: true });
134
+ fs.rmSync(replacedDir, { recursive: true, force: true });
135
+ removed.push(replacedId);
136
+ }
137
+ return { zip_path: remote.zipPath, backup_dir: backupDir, installed, removed };
138
+ }
139
+
140
+ function readSkillMetadata(skillDir) {
141
+ const skillPath = path.join(skillDir, 'SKILL.md');
142
+ if (!fs.existsSync(skillPath)) return { exists: false, name: null, version: null, path: skillPath };
143
+ const text = fs.readFileSync(skillPath, 'utf8');
144
+ const nameMatch = text.match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m);
145
+ const versionMatch = text.match(/^version:\s*["']?([^"'\n]+)["']?\s*$/m);
146
+ return {
147
+ exists: true,
148
+ name: nameMatch ? nameMatch[1].trim() : null,
149
+ version: versionMatch ? versionMatch[1].trim() : null,
150
+ path: skillPath,
151
+ };
152
+ }
153
+
154
+ function readLocalVersion(skillsRoot) {
155
+ const meta = readSkillMetadata(path.join(skillsRoot, SKILL_ID));
156
+ return { exists: meta.exists, version: meta.version, path: meta.path };
157
+ }
158
+
159
+ function compareVersions(a, b) {
160
+ const pa = String(a || '0').split(/[.-]/).map((part) => (/^\d+$/.test(part) ? Number(part) : part));
161
+ const pb = String(b || '0').split(/[.-]/).map((part) => (/^\d+$/.test(part) ? Number(part) : part));
162
+ const len = Math.max(pa.length, pb.length);
163
+ for (let i = 0; i < len; i++) {
164
+ const va = pa[i] == null ? 0 : pa[i];
165
+ const vb = pb[i] == null ? 0 : pb[i];
166
+ if (va === vb) continue;
167
+ if (typeof va === 'number' && typeof vb === 'number') return va > vb ? 1 : -1;
168
+ return String(va) > String(vb) ? 1 : -1;
169
+ }
170
+ return 0;
171
+ }
172
+
173
+ function resolveEnvironment(requested, skillsRoot) {
174
+ if (requested === 'local' || requested === 'cloud') return requested;
175
+ if (requested !== 'auto') fail('--environment must be auto, local, or cloud', 'E_INVALID_ARGS');
176
+ const explicit = String(process.env.MAGIC_BUILDER_UPDATE_ENV || process.env.CODEX_RUNTIME_ENV || '').trim().toLowerCase();
177
+ if (explicit === 'cloud' || explicit === 'remote') return 'cloud';
178
+ if (explicit === 'local') return 'local';
179
+ try { fs.accessSync(skillsRoot, fs.constants.W_OK); } catch { return 'cloud'; }
180
+ return 'local';
181
+ }
182
+
183
+ function runUnzip(zipPath, destDir) {
184
+ const result = spawnSync('unzip', ['-q', zipPath, '-d', destDir], { encoding: 'utf8' });
185
+ if (result.status !== 0) throw new Error(`unzip failed: ${result.stderr || result.stdout || `exit ${result.status}`}`);
186
+ }
187
+
188
+ function ensureSafeSkillId(id) {
189
+ if (!/^[a-z0-9][a-z0-9-]*$/i.test(id)) throw new Error(`Unsafe skill id in update package: ${id}`);
190
+ }
191
+
192
+ module.exports = { run };
package/src/index.js ADDED
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ config: require('./lib/config'),
5
+ auth: require('./lib/auth'),
6
+ http: require('./lib/http'),
7
+ output: require('./lib/output'),
8
+ commands: {
9
+ auth: require('./commands/auth'),
10
+ page: require('./commands/page'),
11
+ faas: require('./commands/faas'),
12
+ asset: require('./commands/asset'),
13
+ link: require('./commands/link'),
14
+ doc: require('./commands/doc'),
15
+ feedback: require('./commands/feedback'),
16
+ skill: require('./commands/skill'),
17
+ doctor: require('./commands/doctor'),
18
+ config: require('./commands/config'),
19
+ },
20
+ };
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const TOKEN_FILE = path.join(os.homedir(), '.magic-token');
8
+ const PROJECT_TOKEN_FILE = path.join(process.cwd(), '.magic-token');
9
+
10
+ function getToken(opts = {}) {
11
+ if (opts.token) return Promise.resolve(opts.token);
12
+ if (process.env.MAGIC_TOKEN) return Promise.resolve(process.env.MAGIC_TOKEN);
13
+
14
+ for (const file of [TOKEN_FILE, PROJECT_TOKEN_FILE]) {
15
+ if (!fs.existsSync(file)) continue;
16
+ try {
17
+ const t = fs.readFileSync(file, 'utf8').trim();
18
+ if (t) return Promise.resolve(t);
19
+ } catch (_) {}
20
+ }
21
+
22
+ return Promise.reject(new Error('Not logged in. Run: magic-builder auth login'));
23
+ }
24
+
25
+ function setToken(value) {
26
+ fs.writeFileSync(TOKEN_FILE, value.trim(), { mode: 0o600 });
27
+ }
28
+
29
+ function showToken() {
30
+ if (process.env.MAGIC_TOKEN) {
31
+ return mask(process.env.MAGIC_TOKEN);
32
+ }
33
+ for (const file of [TOKEN_FILE, PROJECT_TOKEN_FILE]) {
34
+ if (!fs.existsSync(file)) continue;
35
+ try {
36
+ const t = fs.readFileSync(file, 'utf8').trim();
37
+ if (t) return mask(t);
38
+ } catch (_) {}
39
+ }
40
+ return null;
41
+ }
42
+
43
+ function mask(t) {
44
+ if (t.length <= 8) return '****';
45
+ return t.slice(0, 4) + '****' + t.slice(-4);
46
+ }
47
+
48
+ module.exports = { getToken, setToken, showToken, TOKEN_FILE, PROJECT_TOKEN_FILE };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const DEFAULT_BASE_URL = 'https://magic.solutionsuite.cn';
8
+ const APPS_CONFIG_FILE = '.magic-apps.json';
9
+
10
+ function normalizeMagicBaseUrl(value) {
11
+ const raw = String(value || DEFAULT_BASE_URL).trim().replace(/\/+$/, '');
12
+ if (!raw) return DEFAULT_BASE_URL;
13
+ return /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
14
+ }
15
+
16
+ function getBaseUrl(opts = {}) {
17
+ return normalizeMagicBaseUrl(opts.baseUrl || process.env.MAGIC_BASE_URL);
18
+ }
19
+
20
+ function getAppsConfigPath() {
21
+ return path.join(process.cwd(), APPS_CONFIG_FILE);
22
+ }
23
+
24
+ function loadAppsConfig() {
25
+ const p = getAppsConfigPath();
26
+ if (fs.existsSync(p)) {
27
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) {}
28
+ }
29
+ return {};
30
+ }
31
+
32
+ function saveAppsConfig(config) {
33
+ fs.writeFileSync(getAppsConfigPath(), JSON.stringify(config, null, 2));
34
+ }
35
+
36
+ module.exports = { DEFAULT_BASE_URL, normalizeMagicBaseUrl, getBaseUrl, loadAppsConfig, saveAppsConfig };
@@ -0,0 +1,175 @@
1
+ 'use strict';
2
+
3
+ const VERSION = require('../../package.json').version;
4
+
5
+ const USAGE = `@USAGE magic-builder
6
+
7
+ BRIEF: CLI for Magic Builder (妙笔).
8
+
9
+ SYNTAX:
10
+ magic-builder <command> <subcommand> [options]
11
+
12
+ COMMANDS:
13
+ auth Login and manage Magic developer tokens
14
+ page Publish, list, and export Magic pages
15
+ faas Publish Magic FaaS functions
16
+ asset Upload files to TOS
17
+ link Generate Magic share links
18
+ doc Create or append Feishu Doc HTML Box apps
19
+ feedback Create Magic feedback records
20
+ skill Check or update the Magic Builder skill package
21
+ doctor Check local CLI environment
22
+ config Show resolved CLI configuration
23
+
24
+ EXAMPLES:
25
+ magic-builder auth login
26
+ magic-builder page publish app.html --title "Dashboard"
27
+ magic-builder page list --scope mine
28
+ magic-builder page export --id recxxx --out app.html
29
+ magic-builder faas publish handler.js --name report-api
30
+ magic-builder asset upload logo.png
31
+ magic-builder link create --title "Weekly Report"
32
+ magic-builder doc create --html app.html --title "Demo"
33
+ magic-builder feedback create --feedback "打不开页面"
34
+ magic-builder skill install
35
+ magic-builder skill check-update
36
+
37
+ ALIASES:
38
+ magic-cli and miaobi are equivalent executable names.
39
+ `;
40
+
41
+ const HELP = `@HELP magic-builder
42
+
43
+ BRIEF: Unified CLI for the Magic Builder platform.
44
+
45
+ SYNTAX:
46
+ magic-builder <command> <subcommand> [options]
47
+
48
+ GLOBAL OPTIONS:
49
+ --base-url, --magic-base-url <url> Magic service base URL
50
+ --token <token> Auth token override
51
+ --format json|table|plain Output format, default json
52
+ --quiet, -q Suppress progress messages
53
+ --version, -v Show version
54
+ --help, -h Show help
55
+ --man Show full manual
56
+
57
+ COMMANDS:
58
+ auth login [--no-open] [--timeout <seconds>] [--interval <seconds>]
59
+ auth set <token>
60
+ auth show
61
+
62
+ page publish <html> --title <title> [--id <id>] [--open-source]
63
+ page list [--title <keyword>] [--scope mine|public]
64
+ page export --id <id> --out <file|dir> [--scope mine|public]
65
+ page export --title <keyword> --out <dir> [--all] [--scope mine|public]
66
+
67
+ faas publish <file.js> --name <name> [--id <id>]
68
+ faas publish --code <code> --name <name> [--id <id>]
69
+
70
+ asset upload <file> [--key <key>] [--content-type <mime>]
71
+
72
+ link create --title <text>
73
+ link create --fid <id>
74
+
75
+ doc create --html <file> --title <title> [--summary <text>] [--as bot|user]
76
+ doc append --html <file> --doc-token <token> [--as bot|user]
77
+
78
+ feedback create --feedback <text> [--title <title>] [--summary <text>]
79
+ feedback create --feedback-file <file> [--dry-run]
80
+
81
+ skill install [--environment auto|local|cloud] [--skills-root <dir>]
82
+ skill check-update [--environment auto|local|cloud] [--skills-root <dir>]
83
+ skill update [--environment auto|local|cloud] [--skills-root <dir>]
84
+
85
+ doctor
86
+ config get
87
+ `;
88
+
89
+ const MAN = `${HELP}
90
+
91
+ AUTHENTICATION:
92
+ Token lookup order: --token flag, MAGIC_TOKEN environment variable, ~/.magic-token.
93
+ Use "magic-builder auth login" for browser-based Magic developer token auth.
94
+
95
+ OUTPUT:
96
+ json is the default and is intended for agents and scripts.
97
+ plain prints key-value pairs or command-specific plain text where available.
98
+ table prints compact human-readable key-value output.
99
+
100
+ VERSION:
101
+ ${VERSION}
102
+ `;
103
+
104
+ const SUBCOMMAND_HELP = {
105
+ auth: `@HELP magic-builder/auth
106
+
107
+ SYNTAX:
108
+ magic-builder auth login [--no-open] [--timeout <seconds>] [--interval <seconds>]
109
+ magic-builder auth set <token>
110
+ magic-builder auth show
111
+ `,
112
+ page: `@HELP magic-builder/page
113
+
114
+ SYNTAX:
115
+ magic-builder page publish <html> --title <title> [--id <id>] [--open-source]
116
+ magic-builder page list [--title <keyword>] [--scope mine|public]
117
+ magic-builder page export --id <id> --out <file|dir> [--scope mine|public]
118
+ magic-builder page export --title <keyword> --out <dir> [--all] [--scope mine|public]
119
+ `,
120
+ faas: `@HELP magic-builder/faas
121
+
122
+ SYNTAX:
123
+ magic-builder faas publish <file.js> --name <name> [--id <id>]
124
+ magic-builder faas publish --code <code> --name <name> [--id <id>]
125
+ `,
126
+ asset: `@HELP magic-builder/asset
127
+
128
+ SYNTAX:
129
+ magic-builder asset upload <file> [--key <key>] [--content-type <mime>]
130
+ `,
131
+ link: `@HELP magic-builder/link
132
+
133
+ SYNTAX:
134
+ magic-builder link create --title <text>
135
+ magic-builder link create --fid <id>
136
+ `,
137
+ doc: `@HELP magic-builder/doc
138
+
139
+ SYNTAX:
140
+ magic-builder doc create --html <file> --title <title> [--summary <text>] [--as bot|user]
141
+ magic-builder doc append --html <file> --doc-token <token> [--as bot|user]
142
+ `,
143
+ feedback: `@HELP magic-builder/feedback
144
+
145
+ SYNTAX:
146
+ magic-builder feedback create --feedback <text>
147
+ magic-builder feedback create --feedback-file <file>
148
+ `,
149
+ skill: `@HELP magic-builder/skill
150
+
151
+ SYNTAX:
152
+ magic-builder skill install [--environment auto|local|cloud] [--skills-root <dir>]
153
+ magic-builder skill check-update [--environment auto|local|cloud] [--skills-root <dir>]
154
+ magic-builder skill update [--environment auto|local|cloud] [--skills-root <dir>]
155
+ `,
156
+ doctor: `@HELP magic-builder/doctor
157
+
158
+ SYNTAX:
159
+ magic-builder doctor
160
+ `,
161
+ config: `@HELP magic-builder/config
162
+
163
+ SYNTAX:
164
+ magic-builder config get
165
+ `,
166
+ };
167
+
168
+ function renderHelp(level, command) {
169
+ if (level === 'man') return MAN;
170
+ if (level === 'help' && command && SUBCOMMAND_HELP[command]) return SUBCOMMAND_HELP[command];
171
+ if (level === 'help') return HELP;
172
+ return USAGE;
173
+ }
174
+
175
+ module.exports = { renderHelp, USAGE, HELP, MAN, SUBCOMMAND_HELP };
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ const MAX_RETRIES = 2;
4
+ const TIMEOUT_MS = 30000;
5
+
6
+ class HttpError extends Error {
7
+ constructor(message, code, status, body) {
8
+ super(message);
9
+ this.code = code;
10
+ this.status = status;
11
+ this.body = body;
12
+ }
13
+ }
14
+
15
+ async function request(url, opts = {}) {
16
+ const { method = 'GET', headers = {}, body, token, timeout = TIMEOUT_MS } = opts;
17
+
18
+ const h = { ...headers };
19
+ if (token) h['Authorization'] = `Bearer ${token}`;
20
+ if (body && !h['Content-Type']) h['Content-Type'] = 'application/json';
21
+
22
+ let lastErr;
23
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
24
+ const controller = new AbortController();
25
+ const timer = setTimeout(() => controller.abort(), timeout);
26
+ try {
27
+ const res = await fetch(url, {
28
+ method,
29
+ headers: h,
30
+ body: typeof body === 'string' ? body : body instanceof Buffer ? body : body ? JSON.stringify(body) : undefined,
31
+ signal: controller.signal,
32
+ });
33
+
34
+ clearTimeout(timer);
35
+
36
+ if (res.status === 401) {
37
+ throw new HttpError('Authentication failed', 'E_AUTH_FAILED', 401);
38
+ }
39
+
40
+ const text = await res.text();
41
+ let json;
42
+ try { json = JSON.parse(text); } catch (_) { json = null; }
43
+
44
+ if (!res.ok) {
45
+ if (res.status >= 500 && attempt < MAX_RETRIES) {
46
+ lastErr = new HttpError(`Server error ${res.status}`, 'E_NETWORK', res.status, text);
47
+ await sleep(1000 * (attempt + 1));
48
+ continue;
49
+ }
50
+ throw new HttpError(json?.msg || `HTTP ${res.status}`, 'E_REQUEST_FAILED', res.status, json || text);
51
+ }
52
+
53
+ return json || text;
54
+ } catch (err) {
55
+ clearTimeout(timer);
56
+ if (err instanceof HttpError) throw err;
57
+ if (err.name === 'AbortError') {
58
+ if (attempt < MAX_RETRIES) {
59
+ lastErr = new HttpError('Request timeout', 'E_NETWORK', 0);
60
+ await sleep(1000 * (attempt + 1));
61
+ continue;
62
+ }
63
+ throw new HttpError('Request timeout', 'E_NETWORK', 0);
64
+ }
65
+ if (attempt < MAX_RETRIES) {
66
+ lastErr = err;
67
+ await sleep(1000 * (attempt + 1));
68
+ continue;
69
+ }
70
+ throw new HttpError(err.message || 'Network error', 'E_NETWORK', 0);
71
+ }
72
+ }
73
+ throw lastErr;
74
+ }
75
+
76
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
77
+
78
+ module.exports = { request, HttpError };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const MIME_MAP = {
4
+ '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
5
+ '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
6
+ '.ico': 'image/x-icon', '.bmp': 'image/bmp', '.tiff': 'image/tiff',
7
+ '.mp4': 'video/mp4', '.webm': 'video/webm', '.mov': 'video/quicktime',
8
+ '.avi': 'video/x-msvideo', '.mkv': 'video/x-matroska',
9
+ '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.ogg': 'audio/ogg',
10
+ '.flac': 'audio/flac', '.aac': 'audio/aac',
11
+ '.pdf': 'application/pdf', '.zip': 'application/zip',
12
+ '.gz': 'application/gzip', '.tar': 'application/x-tar',
13
+ '.json': 'application/json', '.xml': 'application/xml',
14
+ '.csv': 'text/csv', '.txt': 'text/plain',
15
+ '.html': 'text/html', '.htm': 'text/html',
16
+ '.css': 'text/css', '.js': 'application/javascript',
17
+ '.wasm': 'application/wasm', '.ttf': 'font/ttf',
18
+ '.woff': 'font/woff', '.woff2': 'font/woff2',
19
+ '.doc': 'application/msword', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
20
+ '.xls': 'application/vnd.ms-excel', '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
21
+ '.ppt': 'application/vnd.ms-powerpoint', '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
22
+ };
23
+
24
+ function getMimeType(filePath) {
25
+ const ext = require('path').extname(filePath).toLowerCase();
26
+ return MIME_MAP[ext] || 'application/octet-stream';
27
+ }
28
+
29
+ module.exports = { MIME_MAP, getMimeType };