magic-builder 0.1.2 → 0.1.3
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 +46 -5
- package/bin/magic-builder.js +5 -1
- package/package.json +1 -1
- package/src/commands/config.js +3 -1
- package/src/commands/doctor.js +12 -6
- package/src/commands/faas.js +49 -2
- package/src/commands/file.js +93 -0
- package/src/commands/page.js +34 -1
- package/src/commands/update.js +98 -0
- package/src/commands/version.js +14 -0
- package/src/index.js +3 -1
- package/src/lib/auth.js +7 -4
- package/src/lib/config.js +9 -4
- package/src/lib/help.js +50 -8
- package/src/lib/multipart.js +31 -5
- package/src/commands/asset.js +0 -38
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# magic-builder
|
|
2
2
|
|
|
3
|
-
CLI for Magic Builder (妙笔): publish HTML pages, deploy FaaS functions, upload
|
|
3
|
+
CLI for Magic Builder (妙笔): publish HTML pages, deploy FaaS functions, upload files, generate links, and install the Magic Builder Codex skill.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -29,6 +29,15 @@ npx magic-builder help
|
|
|
29
29
|
bunx magic-builder help
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
Update the CLI:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
magic-builder update
|
|
36
|
+
magic-builder update --manager bun
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`update` detects npm or Bun automatically; `--manager` is only for overriding.
|
|
40
|
+
|
|
32
41
|
## Authentication
|
|
33
42
|
|
|
34
43
|
Most write operations need a Magic developer token.
|
|
@@ -48,8 +57,22 @@ Token lookup order:
|
|
|
48
57
|
|
|
49
58
|
1. `--token`
|
|
50
59
|
2. `MAGIC_TOKEN`
|
|
51
|
-
3. `~/.magic-token`
|
|
52
|
-
4.
|
|
60
|
+
3. `~/.magic-builder/magic-token`
|
|
61
|
+
4. legacy `~/.magic-token`
|
|
62
|
+
5. project `.magic-token`
|
|
63
|
+
|
|
64
|
+
CLI state is stored under:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
~/.magic-builder/
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This includes:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
magic-token
|
|
74
|
+
magic-apps.json
|
|
75
|
+
```
|
|
53
76
|
|
|
54
77
|
## Common Commands
|
|
55
78
|
|
|
@@ -67,16 +90,33 @@ magic-builder page list --scope public --title demo
|
|
|
67
90
|
magic-builder page export --id <id> --out app.html
|
|
68
91
|
```
|
|
69
92
|
|
|
93
|
+
Delete a page:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
magic-builder page delete --id <id>
|
|
97
|
+
```
|
|
98
|
+
|
|
70
99
|
Deploy a FaaS function:
|
|
71
100
|
|
|
72
101
|
```bash
|
|
73
102
|
magic-builder faas publish handler.js --name report-api
|
|
74
103
|
```
|
|
75
104
|
|
|
76
|
-
|
|
105
|
+
List or delete FaaS functions:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
magic-builder faas list
|
|
109
|
+
magic-builder faas list --title report
|
|
110
|
+
magic-builder faas delete --id <id>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Upload, list, or delete files:
|
|
77
114
|
|
|
78
115
|
```bash
|
|
79
|
-
magic-builder
|
|
116
|
+
magic-builder file upload logo.png
|
|
117
|
+
magic-builder file list
|
|
118
|
+
magic-builder file list --title logo
|
|
119
|
+
magic-builder file delete --id <id>
|
|
80
120
|
```
|
|
81
121
|
|
|
82
122
|
Generate a Magic link:
|
|
@@ -104,6 +144,7 @@ magic-builder skill update
|
|
|
104
144
|
Check local environment:
|
|
105
145
|
|
|
106
146
|
```bash
|
|
147
|
+
magic-builder version
|
|
107
148
|
magic-builder doctor
|
|
108
149
|
magic-builder config get
|
|
109
150
|
```
|
package/bin/magic-builder.js
CHANGED
|
@@ -8,11 +8,13 @@ const COMMANDS = {
|
|
|
8
8
|
auth: require('../src/commands/auth'),
|
|
9
9
|
page: require('../src/commands/page'),
|
|
10
10
|
faas: require('../src/commands/faas'),
|
|
11
|
-
|
|
11
|
+
file: require('../src/commands/file'),
|
|
12
12
|
link: require('../src/commands/link'),
|
|
13
13
|
doc: require('../src/commands/doc'),
|
|
14
14
|
feedback: require('../src/commands/feedback'),
|
|
15
15
|
skill: require('../src/commands/skill'),
|
|
16
|
+
version: require('../src/commands/version'),
|
|
17
|
+
update: require('../src/commands/update'),
|
|
16
18
|
doctor: require('../src/commands/doctor'),
|
|
17
19
|
config: require('../src/commands/config'),
|
|
18
20
|
};
|
|
@@ -45,11 +47,13 @@ function parseGlobalArgs(argv) {
|
|
|
45
47
|
opts.token = argv[++i];
|
|
46
48
|
} else if (a === '--format') {
|
|
47
49
|
opts.format = argv[++i];
|
|
50
|
+
opts.formatExplicit = true;
|
|
48
51
|
} else if (a === '--quiet' || a === '-q') {
|
|
49
52
|
opts.quiet = true;
|
|
50
53
|
} else if (a.startsWith('--') && a.includes('=')) {
|
|
51
54
|
const eq = a.indexOf('=');
|
|
52
55
|
opts[toCamel(a.slice(2, eq))] = a.slice(eq + 1);
|
|
56
|
+
if (a.slice(2, eq) === 'format') opts.formatExplicit = true;
|
|
53
57
|
} else if (a.startsWith('--')) {
|
|
54
58
|
const name = a.slice(2);
|
|
55
59
|
const next = argv[i + 1];
|
package/package.json
CHANGED
package/src/commands/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { getBaseUrl } = require('../lib/config');
|
|
3
|
+
const { CONFIG_DIR, getAppsConfigPath, getBaseUrl } = require('../lib/config');
|
|
4
4
|
const { TOKEN_FILE, showToken } = require('../lib/auth');
|
|
5
5
|
const { success, fail } = require('../lib/output');
|
|
6
6
|
|
|
@@ -9,7 +9,9 @@ async function run(args, opts) {
|
|
|
9
9
|
if (sub !== 'get') fail('Usage: magic-builder config get', 'E_INVALID_ARGS');
|
|
10
10
|
success({
|
|
11
11
|
base_url: getBaseUrl(opts),
|
|
12
|
+
config_dir: CONFIG_DIR,
|
|
12
13
|
token_file: TOKEN_FILE,
|
|
14
|
+
apps_config_file: getAppsConfigPath(),
|
|
13
15
|
token: showToken() || '',
|
|
14
16
|
}, opts);
|
|
15
17
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const { spawnSync } = require('child_process');
|
|
5
|
-
const { getBaseUrl } = require('../lib/config');
|
|
5
|
+
const { CONFIG_DIR, getAppsConfigPath, getBaseUrl } = require('../lib/config');
|
|
6
6
|
const { showToken, TOKEN_FILE } = require('../lib/auth');
|
|
7
7
|
const { success } = require('../lib/output');
|
|
8
8
|
|
|
@@ -10,8 +10,10 @@ async function run(args, opts) {
|
|
|
10
10
|
const checks = [];
|
|
11
11
|
checks.push({ name: 'node', ok: true, detail: process.version });
|
|
12
12
|
checks.push({ name: 'base_url', ok: true, detail: getBaseUrl(opts) });
|
|
13
|
+
checks.push({ name: 'config_dir_writable', ok: canWritePath(CONFIG_DIR), detail: CONFIG_DIR });
|
|
14
|
+
checks.push({ name: 'apps_config_file', ok: true, detail: getAppsConfigPath() });
|
|
13
15
|
checks.push({ name: 'token', ok: !!showToken(), detail: showToken() ? `configured at ${process.env.MAGIC_TOKEN ? 'MAGIC_TOKEN' : TOKEN_FILE}` : 'not configured' });
|
|
14
|
-
checks.push({ name: 'token_file_writable', ok:
|
|
16
|
+
checks.push({ name: 'token_file_writable', ok: canWritePath(TOKEN_FILE), detail: TOKEN_FILE });
|
|
15
17
|
checks.push({ name: 'lark-cli', ...commandCheck('lark-cli') });
|
|
16
18
|
checks.push({ name: 'unzip', ...commandCheck('unzip') });
|
|
17
19
|
success({ ok: checks.every((item) => item.ok), checks }, opts);
|
|
@@ -25,13 +27,17 @@ function commandCheck(command) {
|
|
|
25
27
|
};
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
function
|
|
30
|
+
function canWritePath(targetPath) {
|
|
29
31
|
try {
|
|
30
|
-
if (fs.existsSync(
|
|
31
|
-
fs.accessSync(
|
|
32
|
+
if (fs.existsSync(targetPath)) {
|
|
33
|
+
fs.accessSync(targetPath, fs.constants.W_OK);
|
|
32
34
|
return true;
|
|
33
35
|
}
|
|
34
|
-
|
|
36
|
+
const dir = require('path').extname(targetPath) ? require('path').dirname(targetPath) : targetPath;
|
|
37
|
+
if (!fs.existsSync(dir)) {
|
|
38
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
39
|
+
}
|
|
40
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
35
41
|
return true;
|
|
36
42
|
} catch {
|
|
37
43
|
return false;
|
package/src/commands/faas.js
CHANGED
|
@@ -9,8 +9,13 @@ const { success, fail, failFromHttpError } = require('../lib/output');
|
|
|
9
9
|
|
|
10
10
|
async function run(args, opts) {
|
|
11
11
|
const sub = args[0];
|
|
12
|
-
if (sub
|
|
13
|
-
|
|
12
|
+
if (sub === 'publish') return publish(args.slice(1), opts);
|
|
13
|
+
if (sub === 'list') return list(opts);
|
|
14
|
+
if (sub === 'delete') return deleteFaas(opts);
|
|
15
|
+
fail('Usage: magic-builder faas <publish|list|delete>', 'E_INVALID_ARGS');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function publish(args, opts) {
|
|
14
19
|
let code;
|
|
15
20
|
|
|
16
21
|
if (opts.code) {
|
|
@@ -61,6 +66,48 @@ async function run(args, opts) {
|
|
|
61
66
|
}, opts);
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
async function list(opts) {
|
|
70
|
+
let token;
|
|
71
|
+
try { token = await getToken(opts); } catch (e) { fail(e.message, 'E_NO_TOKEN', 2); }
|
|
72
|
+
const baseUrl = getBaseUrl(opts);
|
|
73
|
+
const url = new URL('/api/faas', baseUrl);
|
|
74
|
+
if (opts.title) url.searchParams.set('title', opts.title);
|
|
75
|
+
if (opts.q) url.searchParams.set('q', opts.q);
|
|
76
|
+
let res;
|
|
77
|
+
try {
|
|
78
|
+
res = await request(String(url), { token });
|
|
79
|
+
} catch (e) { failFromHttpError(e); }
|
|
80
|
+
if (res.code !== 0) fail(res.msg || 'FaaS list failed', 'E_LIST_FAILED');
|
|
81
|
+
const data = res.data || {};
|
|
82
|
+
if (opts.format === 'plain') {
|
|
83
|
+
const rows = (data.records || []).map((record) => {
|
|
84
|
+
const id = record.id || String(record.record_id || '').replace(/^rec/, '');
|
|
85
|
+
const modified = record.modify_time ? ` modified=${record.modify_time}` : '';
|
|
86
|
+
return `${id} ${record.name || '(unnamed)'}${modified} ${baseUrl}/api/faas/${id}`;
|
|
87
|
+
});
|
|
88
|
+
success(rows.join('\n'), opts);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
success(data, opts);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function deleteFaas(opts) {
|
|
95
|
+
const id = opts.id;
|
|
96
|
+
if (!id) fail('Missing --id. Usage: magic-builder faas delete --id <id>', 'E_INVALID_ARGS');
|
|
97
|
+
let token;
|
|
98
|
+
try { token = await getToken(opts); } catch (e) { fail(e.message, 'E_NO_TOKEN', 2); }
|
|
99
|
+
const baseUrl = getBaseUrl(opts);
|
|
100
|
+
let res;
|
|
101
|
+
try {
|
|
102
|
+
res = await request(`${baseUrl}/api/faas?id=${encodeURIComponent(id)}`, {
|
|
103
|
+
method: 'DELETE',
|
|
104
|
+
token,
|
|
105
|
+
});
|
|
106
|
+
} catch (e) { failFromHttpError(e); }
|
|
107
|
+
if (res.code !== 0) fail(res.msg || 'FaaS delete failed', 'E_DELETE_FAILED');
|
|
108
|
+
success(res.data || res, opts);
|
|
109
|
+
}
|
|
110
|
+
|
|
64
111
|
function generateName(filePath) {
|
|
65
112
|
if (!filePath) return `func_${Date.now().toString(36)}`;
|
|
66
113
|
const base = path.basename(filePath, path.extname(filePath));
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { getToken } = require('../lib/auth');
|
|
5
|
+
const { getBaseUrl } = require('../lib/config');
|
|
6
|
+
const { request } = require('../lib/http');
|
|
7
|
+
const { shouldUseMultipart, uploadSingle, uploadMultipart } = require('../lib/multipart');
|
|
8
|
+
const { success, fail, failFromHttpError } = require('../lib/output');
|
|
9
|
+
|
|
10
|
+
async function run(args, opts) {
|
|
11
|
+
const sub = args[0];
|
|
12
|
+
if (sub === 'upload') return upload(args.slice(1), opts);
|
|
13
|
+
if (sub === 'list') return list(opts);
|
|
14
|
+
if (sub === 'delete') return deleteFile(opts);
|
|
15
|
+
fail('Usage: magic-builder file <upload|list|delete>', 'E_INVALID_ARGS');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function upload(args, opts) {
|
|
19
|
+
const file = args[0];
|
|
20
|
+
if (!file) fail('Missing file argument. Usage: magic-builder file upload <file>', 'E_INVALID_ARGS');
|
|
21
|
+
if (!fs.existsSync(file)) fail(`File not found: ${file}`, 'E_NOT_FOUND');
|
|
22
|
+
|
|
23
|
+
const stat = fs.statSync(file);
|
|
24
|
+
if (stat.size === 0) fail('File is empty', 'E_EMPTY_FILE');
|
|
25
|
+
|
|
26
|
+
let token;
|
|
27
|
+
try { token = await getToken(opts); } catch (e) { fail(e.message, 'E_NO_TOKEN', 2); }
|
|
28
|
+
|
|
29
|
+
const uploadOpts = {
|
|
30
|
+
baseUrl: getBaseUrl(opts),
|
|
31
|
+
key: opts.key,
|
|
32
|
+
contentType: opts.contentType,
|
|
33
|
+
quiet: opts.quiet,
|
|
34
|
+
token,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let result;
|
|
38
|
+
try {
|
|
39
|
+
result = shouldUseMultipart(stat.size)
|
|
40
|
+
? await uploadMultipart(file, uploadOpts)
|
|
41
|
+
: await uploadSingle(file, uploadOpts);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
if (e.code) failFromHttpError(e);
|
|
44
|
+
fail(e.message, 'E_UPLOAD_FAILED');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
success(result, opts);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function list(opts) {
|
|
51
|
+
let token;
|
|
52
|
+
try { token = await getToken(opts); } catch (e) { fail(e.message, 'E_NO_TOKEN', 2); }
|
|
53
|
+
const baseUrl = getBaseUrl(opts);
|
|
54
|
+
const url = new URL('/api/tos', baseUrl);
|
|
55
|
+
if (opts.title) url.searchParams.set('title', opts.title);
|
|
56
|
+
if (opts.q) url.searchParams.set('q', opts.q);
|
|
57
|
+
|
|
58
|
+
let res;
|
|
59
|
+
try {
|
|
60
|
+
res = await request(String(url), { token });
|
|
61
|
+
} catch (e) { failFromHttpError(e); }
|
|
62
|
+
if (res.code !== 0) fail(res.msg || 'File list failed', 'E_LIST_FAILED');
|
|
63
|
+
|
|
64
|
+
const data = res.data || {};
|
|
65
|
+
if (opts.format === 'plain') {
|
|
66
|
+
const rows = (data.records || []).map((record) => {
|
|
67
|
+
const modified = record.upload_time ? ` uploaded=${record.upload_time}` : '';
|
|
68
|
+
return `${record.id || ''} ${record.filename || '(unnamed)'}${modified} ${record.url || ''}`;
|
|
69
|
+
});
|
|
70
|
+
success(rows.join('\n'), opts);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
success(data, opts);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function deleteFile(opts) {
|
|
77
|
+
const id = opts.id;
|
|
78
|
+
if (!id) fail('Missing --id. Usage: magic-builder file delete --id <id>', 'E_INVALID_ARGS');
|
|
79
|
+
let token;
|
|
80
|
+
try { token = await getToken(opts); } catch (e) { fail(e.message, 'E_NO_TOKEN', 2); }
|
|
81
|
+
const baseUrl = getBaseUrl(opts);
|
|
82
|
+
let res;
|
|
83
|
+
try {
|
|
84
|
+
res = await request(`${baseUrl}/api/tos?id=${encodeURIComponent(id)}`, {
|
|
85
|
+
method: 'DELETE',
|
|
86
|
+
token,
|
|
87
|
+
});
|
|
88
|
+
} catch (e) { failFromHttpError(e); }
|
|
89
|
+
if (res.code !== 0) fail(res.msg || 'File delete failed', 'E_DELETE_FAILED');
|
|
90
|
+
success(res.data || res, opts);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { run };
|
package/src/commands/page.js
CHANGED
|
@@ -16,7 +16,8 @@ async function run(args, opts) {
|
|
|
16
16
|
if (sub === 'publish') return publish(args.slice(1), opts);
|
|
17
17
|
if (sub === 'list') return list(opts);
|
|
18
18
|
if (sub === 'export') return exportApps(opts);
|
|
19
|
-
|
|
19
|
+
if (sub === 'delete') return deletePage(opts);
|
|
20
|
+
fail('Usage: magic-builder page <publish|list|export|delete>', 'E_INVALID_ARGS');
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
async function publish(args, opts) {
|
|
@@ -134,6 +135,24 @@ async function exportApps(opts) {
|
|
|
134
135
|
success({ written: writeExport(opts.out, exports) }, opts);
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
async function deletePage(opts) {
|
|
139
|
+
const id = opts.id;
|
|
140
|
+
if (!id) fail('Missing --id. Usage: magic-builder page delete --id <id>', 'E_INVALID_ARGS');
|
|
141
|
+
let token;
|
|
142
|
+
try { token = await getToken(opts); } catch (e) { fail(e.message, 'E_NO_TOKEN', 2); }
|
|
143
|
+
const baseUrl = getBaseUrl(opts);
|
|
144
|
+
let res;
|
|
145
|
+
try {
|
|
146
|
+
res = await request(`${baseUrl}/api/html-box/${encodeURIComponent(normalizeId(id))}`, {
|
|
147
|
+
method: 'DELETE',
|
|
148
|
+
token,
|
|
149
|
+
});
|
|
150
|
+
} catch (e) { failFromHttpError(e); }
|
|
151
|
+
if (res.code !== 0) fail(res.msg || 'Page delete failed', 'E_DELETE_FAILED');
|
|
152
|
+
removeDeletedPageFromConfig(normalizeId(id));
|
|
153
|
+
success({ ...(res.data || res), deleted: true }, opts);
|
|
154
|
+
}
|
|
155
|
+
|
|
137
156
|
async function searchApps(opts) {
|
|
138
157
|
const baseUrl = getBaseUrl(opts);
|
|
139
158
|
const scope = normalizeScope(opts.scope);
|
|
@@ -228,6 +247,20 @@ function writeExport(out, exports) {
|
|
|
228
247
|
});
|
|
229
248
|
}
|
|
230
249
|
|
|
250
|
+
function removeDeletedPageFromConfig(id) {
|
|
251
|
+
const normalized = normalizeId(id);
|
|
252
|
+
const config = loadAppsConfig();
|
|
253
|
+
let changed = false;
|
|
254
|
+
for (const [key, value] of Object.entries(config)) {
|
|
255
|
+
const record = value || {};
|
|
256
|
+
if (normalizeId(record.id || record.remoteId) === normalized) {
|
|
257
|
+
delete config[key];
|
|
258
|
+
changed = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (changed) saveAppsConfig(config);
|
|
262
|
+
}
|
|
263
|
+
|
|
231
264
|
function hasLikelyInlineLargeResource(html) {
|
|
232
265
|
return /data:[^"'()\s>]+;base64,/i.test(html) || /base64,[A-Za-z0-9+/=]{1024,}/.test(html);
|
|
233
266
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
const { success, fail } = require('../lib/output');
|
|
6
|
+
|
|
7
|
+
const PACKAGE_NAME = 'magic-builder';
|
|
8
|
+
|
|
9
|
+
async function run(_args, opts) {
|
|
10
|
+
const detected = opts.manager
|
|
11
|
+
? { manager: normalizeManager(opts.manager), reason: 'option' }
|
|
12
|
+
: detectPackageManager();
|
|
13
|
+
const manager = detected.manager;
|
|
14
|
+
const command = buildUpdateCommand(manager);
|
|
15
|
+
|
|
16
|
+
if (opts.dryRun) {
|
|
17
|
+
success({ manager, reason: detected.reason, command: command.join(' ') }, opts);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!opts.quiet) {
|
|
22
|
+
process.stderr.write(`Updating ${PACKAGE_NAME} with ${manager}...\n`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result = spawnSync(command[0], command.slice(1), {
|
|
26
|
+
stdio: 'inherit',
|
|
27
|
+
shell: false,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (result.error) fail(result.error.message, 'E_UPDATE_FAILED');
|
|
31
|
+
if (result.status !== 0) fail(`${manager} update failed`, 'E_UPDATE_FAILED', result.status || 1);
|
|
32
|
+
|
|
33
|
+
success({ updated: true, manager, reason: detected.reason, command: command.join(' ') }, opts);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeManager(value) {
|
|
37
|
+
const manager = String(value || '').trim().toLowerCase();
|
|
38
|
+
if (!manager || manager === 'npm') return 'npm';
|
|
39
|
+
if (manager === 'bun') return 'bun';
|
|
40
|
+
fail(`Unsupported package manager: ${value}. Use --manager npm or --manager bun.`, 'E_INVALID_ARGS');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function detectPackageManager() {
|
|
44
|
+
const execPath = realpath(process.argv[1] || '');
|
|
45
|
+
const npmExecPath = realpath(process.env.npm_execpath || '');
|
|
46
|
+
const ua = String(process.env.npm_config_user_agent || '').toLowerCase();
|
|
47
|
+
|
|
48
|
+
if (ua.startsWith('bun/') || npmExecPath.includes('/bun')) {
|
|
49
|
+
return { manager: 'bun', reason: 'bun_user_agent' };
|
|
50
|
+
}
|
|
51
|
+
if (ua.startsWith('npm/') || npmExecPath.includes('/npm')) {
|
|
52
|
+
return { manager: 'npm', reason: 'npm_user_agent' };
|
|
53
|
+
}
|
|
54
|
+
if (process.versions && process.versions.bun) {
|
|
55
|
+
return { manager: 'bun', reason: 'bun_runtime' };
|
|
56
|
+
}
|
|
57
|
+
if (isBunGlobalPath(execPath)) {
|
|
58
|
+
return { manager: 'bun', reason: 'bun_global_path' };
|
|
59
|
+
}
|
|
60
|
+
if (isNpmGlobalPath(execPath)) {
|
|
61
|
+
return { manager: 'npm', reason: 'npm_global_path' };
|
|
62
|
+
}
|
|
63
|
+
if (!commandExists('npm') && commandExists('bun')) {
|
|
64
|
+
return { manager: 'bun', reason: 'bun_available' };
|
|
65
|
+
}
|
|
66
|
+
return { manager: 'npm', reason: 'default' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function realpath(value) {
|
|
70
|
+
if (!value) return '';
|
|
71
|
+
try {
|
|
72
|
+
return fs.realpathSync(value);
|
|
73
|
+
} catch {
|
|
74
|
+
return String(value || '');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isBunGlobalPath(value) {
|
|
79
|
+
const normalized = String(value || '').replace(/\\/g, '/').toLowerCase();
|
|
80
|
+
return normalized.includes('/.bun/install/global/') || normalized.includes('/bun/install/global/');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isNpmGlobalPath(value) {
|
|
84
|
+
const normalized = String(value || '').replace(/\\/g, '/').toLowerCase();
|
|
85
|
+
return normalized.includes('/lib/node_modules/') || normalized.includes('/node_modules/magic-builder/');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function commandExists(command) {
|
|
89
|
+
const result = spawnSync(command, ['--version'], { encoding: 'utf8', stdio: 'ignore' });
|
|
90
|
+
return !result.error && result.status === 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildUpdateCommand(manager) {
|
|
94
|
+
if (manager === 'bun') return ['bun', 'add', '-g', `${PACKAGE_NAME}@latest`];
|
|
95
|
+
return ['npm', 'install', '-g', `${PACKAGE_NAME}@latest`];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { run };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const pkg = require('../../package.json');
|
|
4
|
+
|
|
5
|
+
async function run(_args, opts) {
|
|
6
|
+
if (opts.formatExplicit && opts.format === 'json') {
|
|
7
|
+
process.stdout.write(JSON.stringify({ name: pkg.name, version: pkg.version }, null, 2) + '\n');
|
|
8
|
+
process.exit(0);
|
|
9
|
+
}
|
|
10
|
+
process.stdout.write(pkg.version + '\n');
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { run };
|
package/src/index.js
CHANGED
|
@@ -9,11 +9,13 @@ module.exports = {
|
|
|
9
9
|
auth: require('./commands/auth'),
|
|
10
10
|
page: require('./commands/page'),
|
|
11
11
|
faas: require('./commands/faas'),
|
|
12
|
-
|
|
12
|
+
file: require('./commands/file'),
|
|
13
13
|
link: require('./commands/link'),
|
|
14
14
|
doc: require('./commands/doc'),
|
|
15
15
|
feedback: require('./commands/feedback'),
|
|
16
16
|
skill: require('./commands/skill'),
|
|
17
|
+
version: require('./commands/version'),
|
|
18
|
+
update: require('./commands/update'),
|
|
17
19
|
doctor: require('./commands/doctor'),
|
|
18
20
|
config: require('./commands/config'),
|
|
19
21
|
},
|
package/src/lib/auth.js
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
|
+
const { CONFIG_DIR } = require('./config');
|
|
6
7
|
|
|
7
|
-
const TOKEN_FILE = path.join(
|
|
8
|
+
const TOKEN_FILE = path.join(CONFIG_DIR, 'magic-token');
|
|
9
|
+
const LEGACY_HOME_TOKEN_FILE = path.join(os.homedir(), '.magic-token');
|
|
8
10
|
const PROJECT_TOKEN_FILE = path.join(process.cwd(), '.magic-token');
|
|
9
11
|
|
|
10
12
|
function getToken(opts = {}) {
|
|
11
13
|
if (opts.token) return Promise.resolve(opts.token);
|
|
12
14
|
if (process.env.MAGIC_TOKEN) return Promise.resolve(process.env.MAGIC_TOKEN);
|
|
13
15
|
|
|
14
|
-
for (const file of [TOKEN_FILE, PROJECT_TOKEN_FILE]) {
|
|
16
|
+
for (const file of [TOKEN_FILE, LEGACY_HOME_TOKEN_FILE, PROJECT_TOKEN_FILE]) {
|
|
15
17
|
if (!fs.existsSync(file)) continue;
|
|
16
18
|
try {
|
|
17
19
|
const t = fs.readFileSync(file, 'utf8').trim();
|
|
@@ -23,6 +25,7 @@ function getToken(opts = {}) {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
function setToken(value) {
|
|
28
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
26
29
|
fs.writeFileSync(TOKEN_FILE, value.trim(), { mode: 0o600 });
|
|
27
30
|
}
|
|
28
31
|
|
|
@@ -30,7 +33,7 @@ function showToken() {
|
|
|
30
33
|
if (process.env.MAGIC_TOKEN) {
|
|
31
34
|
return mask(process.env.MAGIC_TOKEN);
|
|
32
35
|
}
|
|
33
|
-
for (const file of [TOKEN_FILE, PROJECT_TOKEN_FILE]) {
|
|
36
|
+
for (const file of [TOKEN_FILE, LEGACY_HOME_TOKEN_FILE, PROJECT_TOKEN_FILE]) {
|
|
34
37
|
if (!fs.existsSync(file)) continue;
|
|
35
38
|
try {
|
|
36
39
|
const t = fs.readFileSync(file, 'utf8').trim();
|
|
@@ -45,4 +48,4 @@ function mask(t) {
|
|
|
45
48
|
return t.slice(0, 4) + '****' + t.slice(-4);
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
module.exports = { getToken, setToken, showToken, TOKEN_FILE, PROJECT_TOKEN_FILE };
|
|
51
|
+
module.exports = { getToken, setToken, showToken, TOKEN_FILE, LEGACY_HOME_TOKEN_FILE, PROJECT_TOKEN_FILE };
|
package/src/lib/config.js
CHANGED
|
@@ -5,7 +5,9 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
|
|
7
7
|
const DEFAULT_BASE_URL = 'https://magic.solutionsuite.cn';
|
|
8
|
-
const
|
|
8
|
+
const CONFIG_DIR = path.join(os.homedir(), '.magic-builder');
|
|
9
|
+
const APPS_CONFIG_FILE = 'magic-apps.json';
|
|
10
|
+
const LEGACY_APPS_CONFIG_FILE = '.magic-apps.json';
|
|
9
11
|
|
|
10
12
|
function normalizeMagicBaseUrl(value) {
|
|
11
13
|
const raw = String(value || DEFAULT_BASE_URL).trim().replace(/\/+$/, '');
|
|
@@ -18,11 +20,13 @@ function getBaseUrl(opts = {}) {
|
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
function getAppsConfigPath() {
|
|
21
|
-
return path.join(
|
|
23
|
+
return path.join(CONFIG_DIR, APPS_CONFIG_FILE);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
function loadAppsConfig() {
|
|
25
|
-
const p = getAppsConfigPath()
|
|
27
|
+
const p = fs.existsSync(getAppsConfigPath())
|
|
28
|
+
? getAppsConfigPath()
|
|
29
|
+
: path.join(process.cwd(), LEGACY_APPS_CONFIG_FILE);
|
|
26
30
|
if (fs.existsSync(p)) {
|
|
27
31
|
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) {}
|
|
28
32
|
}
|
|
@@ -30,7 +34,8 @@ function loadAppsConfig() {
|
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
function saveAppsConfig(config) {
|
|
37
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
33
38
|
fs.writeFileSync(getAppsConfigPath(), JSON.stringify(config, null, 2));
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
module.exports = { DEFAULT_BASE_URL, normalizeMagicBaseUrl, getBaseUrl, loadAppsConfig, saveAppsConfig };
|
|
41
|
+
module.exports = { DEFAULT_BASE_URL, CONFIG_DIR, normalizeMagicBaseUrl, getBaseUrl, getAppsConfigPath, loadAppsConfig, saveAppsConfig };
|
package/src/lib/help.js
CHANGED
|
@@ -15,13 +15,15 @@ SYNTAX:
|
|
|
15
15
|
|
|
16
16
|
COMMANDS:
|
|
17
17
|
auth Login and manage Magic developer tokens
|
|
18
|
-
page Publish, list, and
|
|
19
|
-
faas Publish Magic FaaS functions
|
|
20
|
-
|
|
18
|
+
page Publish, list, export, and delete Magic pages
|
|
19
|
+
faas Publish, list, and delete Magic FaaS functions
|
|
20
|
+
file Upload, list, and delete TOS files
|
|
21
21
|
link Generate Magic share links
|
|
22
22
|
doc Create or append Feishu Doc HTML Box apps
|
|
23
23
|
feedback Create Magic feedback records
|
|
24
24
|
skill Check or update the Magic Builder skill package
|
|
25
|
+
version Show CLI version
|
|
26
|
+
update Update this CLI package
|
|
25
27
|
doctor Check local CLI environment
|
|
26
28
|
config Show resolved CLI configuration
|
|
27
29
|
|
|
@@ -33,13 +35,20 @@ EXAMPLES:
|
|
|
33
35
|
magic-builder page publish app.html --title "Dashboard"
|
|
34
36
|
magic-builder page list --scope mine
|
|
35
37
|
magic-builder page export --id recxxx --out app.html
|
|
38
|
+
magic-builder page delete --id recxxx
|
|
36
39
|
magic-builder faas publish handler.js --name report-api
|
|
37
|
-
magic-builder
|
|
40
|
+
magic-builder faas list
|
|
41
|
+
magic-builder faas delete --id recxxx
|
|
42
|
+
magic-builder file upload logo.png
|
|
43
|
+
magic-builder file list
|
|
44
|
+
magic-builder file delete --id recxxx
|
|
38
45
|
magic-builder link create --title "Weekly Report"
|
|
39
46
|
magic-builder doc create --html app.html --title "Demo"
|
|
40
47
|
magic-builder feedback create --feedback "打不开页面"
|
|
41
48
|
magic-builder skill install
|
|
42
49
|
magic-builder skill check-update
|
|
50
|
+
magic-builder version
|
|
51
|
+
magic-builder update
|
|
43
52
|
|
|
44
53
|
ALIASES:
|
|
45
54
|
magic-cli and miaobi are equivalent executable names.
|
|
@@ -74,11 +83,16 @@ COMMANDS:
|
|
|
74
83
|
page list [--title <keyword>] [--scope mine|public]
|
|
75
84
|
page export --id <id> --out <file|dir> [--scope mine|public]
|
|
76
85
|
page export --title <keyword> --out <dir> [--all] [--scope mine|public]
|
|
86
|
+
page delete --id <id>
|
|
77
87
|
|
|
78
88
|
faas publish <file.js> --name <name> [--id <id>]
|
|
79
89
|
faas publish --code <code> --name <name> [--id <id>]
|
|
90
|
+
faas list [--title <keyword>]
|
|
91
|
+
faas delete --id <id>
|
|
80
92
|
|
|
81
|
-
|
|
93
|
+
file upload <file> [--key <key>] [--content-type <mime>]
|
|
94
|
+
file list [--title <keyword>]
|
|
95
|
+
file delete --id <id>
|
|
82
96
|
|
|
83
97
|
link create --title <text>
|
|
84
98
|
link create --fid <id>
|
|
@@ -93,6 +107,9 @@ COMMANDS:
|
|
|
93
107
|
skill check-update [--environment auto|local|cloud] [--skills-root <dir>]
|
|
94
108
|
skill update [--environment auto|local|cloud] [--skills-root <dir>]
|
|
95
109
|
|
|
110
|
+
version
|
|
111
|
+
update [--manager npm|bun] [--dry-run]
|
|
112
|
+
|
|
96
113
|
doctor
|
|
97
114
|
config get
|
|
98
115
|
`;
|
|
@@ -100,9 +117,15 @@ COMMANDS:
|
|
|
100
117
|
const MAN = `${HELP}
|
|
101
118
|
|
|
102
119
|
AUTHENTICATION:
|
|
103
|
-
Token lookup order: --token flag, MAGIC_TOKEN
|
|
120
|
+
Token lookup order: --token flag, MAGIC_TOKEN, ~/.magic-builder/magic-token,
|
|
121
|
+
legacy ~/.magic-token, and project .magic-token.
|
|
104
122
|
Use "magic-builder auth login" for browser-based Magic developer token auth.
|
|
105
123
|
|
|
124
|
+
CONFIG:
|
|
125
|
+
User-level CLI state is stored under ~/.magic-builder/.
|
|
126
|
+
Token file: ~/.magic-builder/magic-token
|
|
127
|
+
Page publish records: ~/.magic-builder/magic-apps.json
|
|
128
|
+
|
|
106
129
|
OUTPUT:
|
|
107
130
|
json is the default and is intended for agents and scripts.
|
|
108
131
|
plain prints key-value pairs or command-specific plain text where available.
|
|
@@ -127,17 +150,22 @@ SYNTAX:
|
|
|
127
150
|
magic-builder page list [--title <keyword>] [--scope mine|public]
|
|
128
151
|
magic-builder page export --id <id> --out <file|dir> [--scope mine|public]
|
|
129
152
|
magic-builder page export --title <keyword> --out <dir> [--all] [--scope mine|public]
|
|
153
|
+
magic-builder page delete --id <id>
|
|
130
154
|
`,
|
|
131
155
|
faas: `@HELP magic-builder/faas
|
|
132
156
|
|
|
133
157
|
SYNTAX:
|
|
134
158
|
magic-builder faas publish <file.js> --name <name> [--id <id>]
|
|
135
159
|
magic-builder faas publish --code <code> --name <name> [--id <id>]
|
|
160
|
+
magic-builder faas list [--title <keyword>]
|
|
161
|
+
magic-builder faas delete --id <id>
|
|
136
162
|
`,
|
|
137
|
-
|
|
163
|
+
file: `@HELP magic-builder/file
|
|
138
164
|
|
|
139
165
|
SYNTAX:
|
|
140
|
-
magic-builder
|
|
166
|
+
magic-builder file upload <file> [--key <key>] [--content-type <mime>]
|
|
167
|
+
magic-builder file list [--title <keyword>]
|
|
168
|
+
magic-builder file delete --id <id>
|
|
141
169
|
`,
|
|
142
170
|
link: `@HELP magic-builder/link
|
|
143
171
|
|
|
@@ -163,6 +191,20 @@ SYNTAX:
|
|
|
163
191
|
magic-builder skill install [--environment auto|local|cloud] [--skills-root <dir>]
|
|
164
192
|
magic-builder skill check-update [--environment auto|local|cloud] [--skills-root <dir>]
|
|
165
193
|
magic-builder skill update [--environment auto|local|cloud] [--skills-root <dir>]
|
|
194
|
+
`,
|
|
195
|
+
version: `@HELP magic-builder/version
|
|
196
|
+
|
|
197
|
+
SYNTAX:
|
|
198
|
+
magic-builder version
|
|
199
|
+
magic-builder --version
|
|
200
|
+
`,
|
|
201
|
+
update: `@HELP magic-builder/update
|
|
202
|
+
|
|
203
|
+
SYNTAX:
|
|
204
|
+
magic-builder update [--manager npm|bun] [--dry-run]
|
|
205
|
+
|
|
206
|
+
NOTES:
|
|
207
|
+
Package manager is detected automatically. Use --manager only to override.
|
|
166
208
|
`,
|
|
167
209
|
doctor: `@HELP magic-builder/doctor
|
|
168
210
|
|
package/src/lib/multipart.js
CHANGED
|
@@ -12,7 +12,7 @@ function shouldUseMultipart(fileSize) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
async function uploadSingle(filePath, opts = {}) {
|
|
15
|
-
const { baseUrl, key, contentType, quiet } = opts;
|
|
15
|
+
const { baseUrl, key, contentType, quiet, token } = opts;
|
|
16
16
|
const filename = require('path').basename(filePath);
|
|
17
17
|
const mime = contentType || require('./mime').getMimeType(filePath);
|
|
18
18
|
|
|
@@ -38,11 +38,30 @@ async function uploadSingle(filePath, opts = {}) {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
if (!quiet) process.stderr.write('done\n');
|
|
41
|
-
|
|
41
|
+
const record = await request(`${baseUrl}/api/tos`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
token,
|
|
44
|
+
body: {
|
|
45
|
+
action: 'record',
|
|
46
|
+
url,
|
|
47
|
+
key: signRes.data.key,
|
|
48
|
+
filename,
|
|
49
|
+
contentType: mime,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
if (record.code !== 0) throw new Error(record.msg || 'Upload audit failed');
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
id: record.data?.id || '',
|
|
56
|
+
record_id: record.data?.record_id || '',
|
|
57
|
+
url,
|
|
58
|
+
key: signRes.data.key,
|
|
59
|
+
size: buf.length,
|
|
60
|
+
};
|
|
42
61
|
}
|
|
43
62
|
|
|
44
63
|
async function uploadMultipart(filePath, opts = {}) {
|
|
45
|
-
const { baseUrl, key, contentType, quiet } = opts;
|
|
64
|
+
const { baseUrl, key, contentType, quiet, token } = opts;
|
|
46
65
|
const filename = require('path').basename(filePath);
|
|
47
66
|
const mime = contentType || require('./mime').getMimeType(filePath);
|
|
48
67
|
const stat = fs.statSync(filePath);
|
|
@@ -100,13 +119,20 @@ async function uploadMultipart(filePath, opts = {}) {
|
|
|
100
119
|
|
|
101
120
|
const completeRes = await request(`${baseUrl}/api/tos/multipart/complete`, {
|
|
102
121
|
method: 'POST',
|
|
103
|
-
|
|
122
|
+
token,
|
|
123
|
+
body: { uploadId, key: tosKey, parts, filename, contentType: mime },
|
|
104
124
|
});
|
|
105
125
|
|
|
106
126
|
if (completeRes.code !== 0) throw new Error(completeRes.msg || 'Multipart complete failed');
|
|
107
127
|
if (!quiet) process.stderr.write('\rUpload complete. \n');
|
|
108
128
|
|
|
109
|
-
return {
|
|
129
|
+
return {
|
|
130
|
+
id: completeRes.data?.id || '',
|
|
131
|
+
record_id: completeRes.data?.record_id || '',
|
|
132
|
+
url: completeRes.data?.url || url,
|
|
133
|
+
key: tosKey,
|
|
134
|
+
size: stat.size,
|
|
135
|
+
};
|
|
110
136
|
}
|
|
111
137
|
|
|
112
138
|
async function abortMultipart(baseUrl, uploadId, key) {
|
package/src/commands/asset.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const { getBaseUrl } = require('../lib/config');
|
|
5
|
-
const { shouldUseMultipart, uploadSingle, uploadMultipart } = require('../lib/multipart');
|
|
6
|
-
const { success, fail, failFromHttpError } = require('../lib/output');
|
|
7
|
-
|
|
8
|
-
async function run(args, opts) {
|
|
9
|
-
const sub = args[0];
|
|
10
|
-
if (sub !== 'upload') fail('Usage: magic-builder asset upload <file>', 'E_INVALID_ARGS');
|
|
11
|
-
const file = args[1];
|
|
12
|
-
if (!file) fail('Missing file argument. Usage: magic-builder asset upload <file>', 'E_INVALID_ARGS');
|
|
13
|
-
if (!fs.existsSync(file)) fail(`File not found: ${file}`, 'E_NOT_FOUND');
|
|
14
|
-
|
|
15
|
-
const stat = fs.statSync(file);
|
|
16
|
-
if (stat.size === 0) fail('File is empty', 'E_EMPTY_FILE');
|
|
17
|
-
|
|
18
|
-
const uploadOpts = {
|
|
19
|
-
baseUrl: getBaseUrl(opts),
|
|
20
|
-
key: opts.key,
|
|
21
|
-
contentType: opts.contentType,
|
|
22
|
-
quiet: opts.quiet,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
let result;
|
|
26
|
-
try {
|
|
27
|
-
result = shouldUseMultipart(stat.size)
|
|
28
|
-
? await uploadMultipart(file, uploadOpts)
|
|
29
|
-
: await uploadSingle(file, uploadOpts);
|
|
30
|
-
} catch (e) {
|
|
31
|
-
if (e.code) failFromHttpError(e);
|
|
32
|
-
fail(e.message, 'E_UPLOAD_FAILED');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
success(result, opts);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
module.exports = { run };
|