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,99 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { renderHelp } = require('../src/lib/help');
5
+ const { fail } = require('../src/lib/output');
6
+
7
+ const COMMANDS = {
8
+ auth: require('../src/commands/auth'),
9
+ page: require('../src/commands/page'),
10
+ faas: require('../src/commands/faas'),
11
+ asset: require('../src/commands/asset'),
12
+ link: require('../src/commands/link'),
13
+ doc: require('../src/commands/doc'),
14
+ feedback: require('../src/commands/feedback'),
15
+ skill: require('../src/commands/skill'),
16
+ doctor: require('../src/commands/doctor'),
17
+ config: require('../src/commands/config'),
18
+ };
19
+
20
+ function parseGlobalArgs(argv) {
21
+ const opts = { format: 'json', quiet: false };
22
+ const args = [];
23
+ let command = null;
24
+ let i = 0;
25
+
26
+ while (i < argv.length) {
27
+ const a = argv[i];
28
+
29
+ if (!command && COMMANDS[a]) {
30
+ command = a;
31
+ } else if (!command && !a.startsWith('-')) {
32
+ fail(`Unknown command: ${a}. Run: magic-builder --help`, 'E_INVALID_ARGS');
33
+ } else if (a === '--help' || a === '-h') {
34
+ process.stdout.write(renderHelp('help', command) + '\n');
35
+ process.exit(0);
36
+ } else if (a === '--man') {
37
+ process.stdout.write(renderHelp('man') + '\n');
38
+ process.exit(0);
39
+ } else if (a === '--version' || a === '-v') {
40
+ process.stdout.write(require('../package.json').version + '\n');
41
+ process.exit(0);
42
+ } else if (a === '--base-url' || a === '--magic-base-url') {
43
+ opts.baseUrl = argv[++i];
44
+ } else if (a === '--token') {
45
+ opts.token = argv[++i];
46
+ } else if (a === '--format') {
47
+ opts.format = argv[++i];
48
+ } else if (a === '--quiet' || a === '-q') {
49
+ opts.quiet = true;
50
+ } else if (a.startsWith('--') && a.includes('=')) {
51
+ const eq = a.indexOf('=');
52
+ opts[toCamel(a.slice(2, eq))] = a.slice(eq + 1);
53
+ } else if (a.startsWith('--')) {
54
+ const name = a.slice(2);
55
+ const next = argv[i + 1];
56
+ if (next && !next.startsWith('--')) {
57
+ opts[toCamel(name)] = next;
58
+ i++;
59
+ } else {
60
+ opts[toCamel(name)] = true;
61
+ }
62
+ } else if (!a.startsWith('-')) {
63
+ args.push(a);
64
+ } else {
65
+ fail(`Unknown option: ${a}. Run: magic-builder --help`, 'E_INVALID_ARGS');
66
+ }
67
+ i++;
68
+ }
69
+
70
+ return { command, args, opts };
71
+ }
72
+
73
+ function toCamel(value) {
74
+ return String(value || '').replace(/-([a-z])/g, (_, c) => c.toUpperCase());
75
+ }
76
+
77
+ async function main() {
78
+ const argv = process.argv.slice(2);
79
+
80
+ if (argv.length === 0) {
81
+ process.stdout.write(renderHelp('usage') + '\n');
82
+ process.exit(0);
83
+ }
84
+
85
+ const { command, args, opts } = parseGlobalArgs(argv);
86
+
87
+ if (!command) {
88
+ process.stdout.write(renderHelp('usage') + '\n');
89
+ process.exit(0);
90
+ }
91
+
92
+ try {
93
+ await COMMANDS[command].run(args, opts);
94
+ } catch (err) {
95
+ fail(err.message || 'Unexpected error', err.code || 'E_ERROR');
96
+ }
97
+ }
98
+
99
+ main();
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "magic-builder",
3
+ "version": "0.1.0",
4
+ "description": "CLI for Magic Builder — publish pages, functions, and files",
5
+ "bin": {
6
+ "magic-builder": "./bin/magic-builder.js",
7
+ "magic-cli": "./bin/magic-builder.js",
8
+ "miaobi": "./bin/magic-builder.js"
9
+ },
10
+ "main": "./src/index.js",
11
+ "engines": {
12
+ "node": ">=18.0.0"
13
+ },
14
+ "type": "commonjs",
15
+ "files": ["bin/", "src/", "README.md", "LICENSE"],
16
+ "keywords": ["magic-builder", "miaobi", "cli", "faas", "publish"],
17
+ "license": "MIT",
18
+ "publishConfig": {
19
+ "registry": "https://registry.npmjs.org/"
20
+ }
21
+ }
@@ -0,0 +1,38 @@
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 };
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('child_process');
4
+ const { getBaseUrl } = require('../lib/config');
5
+ const { setToken, showToken, TOKEN_FILE } = require('../lib/auth');
6
+ const { request } = require('../lib/http');
7
+ const { success, fail } = require('../lib/output');
8
+
9
+ async function run(args, opts) {
10
+ const sub = args[0];
11
+ if (sub === 'login') return login(opts);
12
+ if (sub === 'set') return set(args, opts);
13
+ if (sub === 'show') return show(opts);
14
+ fail('Usage: magic-builder auth <login|set|show>', 'E_INVALID_ARGS');
15
+ }
16
+
17
+ async function login(opts) {
18
+ const baseUrl = getBaseUrl(opts);
19
+ const timeoutSeconds = Number(opts.timeout || 600);
20
+ const intervalSeconds = Number(opts.interval || 2);
21
+ if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) fail('--timeout must be a positive number', 'E_INVALID_ARGS');
22
+ if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) fail('--interval must be a positive number', 'E_INVALID_ARGS');
23
+
24
+ const start = await request(`${baseUrl}/api/dev-token/auth/start`, {
25
+ method: 'POST',
26
+ body: {},
27
+ });
28
+ const data = start.data || {};
29
+ const requestId = String(data.request_id || '');
30
+ const pollToken = String(data.poll_token || '');
31
+ const authUrl = String(data.auth_url || '');
32
+ if (!requestId || !pollToken || !authUrl) {
33
+ fail(`Invalid auth start response: ${JSON.stringify(start)}`, 'E_AUTH_FAILED', 2);
34
+ }
35
+
36
+ if (!opts.quiet) {
37
+ process.stderr.write(`Open this URL to authorize:\n${authUrl}\n`);
38
+ if (!opts.noOpen) {
39
+ process.stderr.write(openBrowser(authUrl) ? 'Browser opened.\n' : 'Could not open browser automatically.\n');
40
+ }
41
+ process.stderr.write('Waiting for authorization...\n');
42
+ } else if (opts.format === 'plain') {
43
+ process.stdout.write(`${authUrl}\n`);
44
+ }
45
+
46
+ const deadline = Date.now() + timeoutSeconds * 1000;
47
+ while (Date.now() < deadline) {
48
+ const tokenJson = await request(`${baseUrl}/api/dev-token/auth/token`, {
49
+ method: 'POST',
50
+ body: {
51
+ request_id: requestId,
52
+ poll_token: pollToken,
53
+ },
54
+ timeout: 15000,
55
+ });
56
+ const tokenData = tokenJson.data || {};
57
+ const status = String(tokenData.status || '');
58
+ if (status === 'completed') {
59
+ const token = String(tokenData.token || '');
60
+ if (!token) fail(`Completed auth response did not include token: ${JSON.stringify(tokenJson)}`, 'E_AUTH_FAILED', 2);
61
+ setToken(token);
62
+ success({
63
+ status: 'authenticated',
64
+ path: TOKEN_FILE,
65
+ user: String(tokenData.name || tokenData.open_id || '').trim(),
66
+ token: showToken(),
67
+ }, opts);
68
+ return;
69
+ }
70
+ if (status === 'pending') {
71
+ await sleep(intervalSeconds * 1000);
72
+ continue;
73
+ }
74
+ fail(`Unexpected auth status: ${JSON.stringify(tokenJson)}`, 'E_AUTH_FAILED', 2);
75
+ }
76
+ fail('Timed out waiting for authorization. Run auth login again.', 'E_AUTH_TIMEOUT', 2);
77
+ }
78
+
79
+ function set(args, opts) {
80
+ const value = args[1];
81
+ if (!value) fail('Missing token value. Usage: magic-builder auth set <value>', 'E_INVALID_ARGS');
82
+ setToken(value);
83
+ success({ status: 'saved', path: TOKEN_FILE }, opts);
84
+ }
85
+
86
+ function show(opts) {
87
+ const masked = showToken();
88
+ if (!masked) fail('No token configured. Run: magic-builder auth login', 'E_NO_TOKEN', 2);
89
+ success({ token: masked }, opts);
90
+ }
91
+
92
+ function openBrowser(url) {
93
+ const command = process.platform === 'darwin'
94
+ ? 'open'
95
+ : process.platform === 'win32'
96
+ ? 'cmd'
97
+ : 'xdg-open';
98
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
99
+ try {
100
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' });
101
+ child.unref();
102
+ return true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ function sleep(ms) {
109
+ return new Promise((resolve) => setTimeout(resolve, ms));
110
+ }
111
+
112
+ module.exports = { run };
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ const { getBaseUrl } = require('../lib/config');
4
+ const { TOKEN_FILE, showToken } = require('../lib/auth');
5
+ const { success, fail } = require('../lib/output');
6
+
7
+ async function run(args, opts) {
8
+ const sub = args[0];
9
+ if (sub !== 'get') fail('Usage: magic-builder config get', 'E_INVALID_ARGS');
10
+ success({
11
+ base_url: getBaseUrl(opts),
12
+ token_file: TOKEN_FILE,
13
+ token: showToken() || '',
14
+ }, opts);
15
+ }
16
+
17
+ module.exports = { run };
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ const { readFileSync } = require('fs');
4
+ const { execFileSync } = require('child_process');
5
+ const { success, fail } = require('../lib/output');
6
+
7
+ const HTML_BOX_COMPONENT_TYPE_ID = 'blk_6900429af84180025ce76527';
8
+
9
+ async function run(args, opts) {
10
+ const sub = args[0];
11
+ if (sub !== 'create' && sub !== 'append') fail('Usage: magic-builder doc <create|append> --html <file>', 'E_INVALID_ARGS');
12
+ if (!opts.html) fail('Missing --html', 'E_INVALID_ARGS');
13
+ if (sub === 'create' && !opts.title) fail('Missing --title', 'E_INVALID_ARGS');
14
+ if (sub === 'append' && !opts.docToken) fail('Missing --doc-token', 'E_INVALID_ARGS');
15
+
16
+ const html = readFileSync(opts.html, 'utf8');
17
+ const record = buildRecord(opts, html);
18
+ const identity = resolveIdentity(opts.as);
19
+ const created = sub === 'append'
20
+ ? { docToken: opts.docToken, url: `https://bytedance.larkoffice.com/docx/${opts.docToken}` }
21
+ : createDocument(opts.title, opts.summary, identity);
22
+ const htmlBoxBlockId = insertHtmlBox(created.docToken, record, identity);
23
+
24
+ success({
25
+ ok: true,
26
+ identity,
27
+ doc_token: created.docToken,
28
+ doc_url: created.url,
29
+ html_box_block_id: htmlBoxBlockId,
30
+ }, opts);
31
+ }
32
+
33
+ function buildRecord(opts, html) {
34
+ const record = { html };
35
+ const json = readOptionalText(opts.recordJson);
36
+ const js = readOptionalText(opts.recordJs);
37
+ const scripts = splitCsv(opts.recordScripts);
38
+ if (json !== undefined) record.json = json;
39
+ if (js !== undefined) record.js = js;
40
+ if (scripts.length > 0) record.scripts = scripts;
41
+ return record;
42
+ }
43
+
44
+ function resolveIdentity(requestedIdentity) {
45
+ if (requestedIdentity) {
46
+ if (requestedIdentity !== 'bot' && requestedIdentity !== 'user') fail('--as must be bot or user', 'E_INVALID_ARGS');
47
+ return requestedIdentity;
48
+ }
49
+ const status = runJson(['auth', 'status']);
50
+ if (status?.identity === 'user' || status?.identity === 'bot') return status.identity;
51
+ return 'bot';
52
+ }
53
+
54
+ function createDocument(title, summary, identity) {
55
+ const intro = summary || `这个妙笔应用以交互式网页的形式展示「${title}」,可直接在文档中查看和体验。`;
56
+ const content = `<title>${escapeXml(title)}</title><p>${escapeXml(intro)}</p>`;
57
+ const resp = runJson([
58
+ 'docs',
59
+ '+create',
60
+ '--as',
61
+ identity,
62
+ '--api-version',
63
+ 'v2',
64
+ '--content',
65
+ content,
66
+ ]);
67
+ if (!resp?.ok || !resp?.data?.document?.document_id) {
68
+ throw new Error(`failed to create document: ${JSON.stringify(resp)}`);
69
+ }
70
+ return {
71
+ docToken: resp.data.document.document_id,
72
+ url: resp.data.document.url,
73
+ };
74
+ }
75
+
76
+ function insertHtmlBox(docToken, recordObject, identity) {
77
+ const record = JSON.stringify(recordObject);
78
+ const resp = runJson([
79
+ 'api',
80
+ 'POST',
81
+ `/open-apis/docx/v1/documents/${docToken}/blocks/${docToken}/children`,
82
+ '--as',
83
+ identity,
84
+ '--data',
85
+ JSON.stringify({
86
+ children: [{
87
+ block_type: 40,
88
+ add_ons: {
89
+ component_id: '',
90
+ component_type_id: HTML_BOX_COMPONENT_TYPE_ID,
91
+ record,
92
+ },
93
+ }],
94
+ index: -1,
95
+ }),
96
+ ]);
97
+ if (resp?.code !== 0) throw new Error(`failed to insert HTML Box: ${JSON.stringify(resp)}`);
98
+ const block = resp?.data?.children?.[0];
99
+ const returnedRecord = parseRecordValue(block?.add_ons?.record);
100
+ if (!recordIncludes(recordObject, returnedRecord)) {
101
+ throw new Error(`inserted HTML Box record mismatch: ${JSON.stringify(block?.add_ons)}`);
102
+ }
103
+ return block?.block_id || '';
104
+ }
105
+
106
+ function runJson(commandArgs) {
107
+ const out = execFileSync('lark-cli', commandArgs, {
108
+ encoding: 'utf8',
109
+ stdio: ['ignore', 'pipe', 'pipe'],
110
+ });
111
+ return JSON.parse(out);
112
+ }
113
+
114
+ function readOptionalText(file) {
115
+ return file ? readFileSync(file, 'utf8') : undefined;
116
+ }
117
+
118
+ function splitCsv(value) {
119
+ return String(value || '').split(',').map((item) => item.trim()).filter(Boolean);
120
+ }
121
+
122
+ function stableStringify(value) {
123
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
124
+ if (value && typeof value === 'object') {
125
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
126
+ }
127
+ return JSON.stringify(value);
128
+ }
129
+
130
+ function parseRecordValue(value) {
131
+ if (typeof value === 'string') {
132
+ try { return JSON.parse(value); } catch { return null; }
133
+ }
134
+ if (value && typeof value === 'object') return value;
135
+ return null;
136
+ }
137
+
138
+ function recordIncludes(expected, actual) {
139
+ if (!actual || typeof actual !== 'object') return false;
140
+ return Object.keys(expected).every((key) => stableStringify(actual[key]) === stableStringify(expected[key]));
141
+ }
142
+
143
+ function escapeXml(value) {
144
+ return String(value)
145
+ .replaceAll('&', '&amp;')
146
+ .replaceAll('<', '&lt;')
147
+ .replaceAll('>', '&gt;')
148
+ .replaceAll('"', '&quot;')
149
+ .replaceAll("'", '&apos;');
150
+ }
151
+
152
+ module.exports = { run };
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const { spawnSync } = require('child_process');
5
+ const { getBaseUrl } = require('../lib/config');
6
+ const { showToken, TOKEN_FILE } = require('../lib/auth');
7
+ const { success } = require('../lib/output');
8
+
9
+ async function run(args, opts) {
10
+ const checks = [];
11
+ checks.push({ name: 'node', ok: true, detail: process.version });
12
+ checks.push({ name: 'base_url', ok: true, detail: getBaseUrl(opts) });
13
+ 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: canWriteTokenFile(), detail: TOKEN_FILE });
15
+ checks.push({ name: 'lark-cli', ...commandCheck('lark-cli') });
16
+ checks.push({ name: 'unzip', ...commandCheck('unzip') });
17
+ success({ ok: checks.every((item) => item.ok), checks }, opts);
18
+ }
19
+
20
+ function commandCheck(command) {
21
+ const result = spawnSync(command, ['--version'], { encoding: 'utf8' });
22
+ return {
23
+ ok: result.status === 0,
24
+ detail: result.status === 0 ? (result.stdout || result.stderr || '').trim().split(/\r?\n/)[0] : 'not found or failed',
25
+ };
26
+ }
27
+
28
+ function canWriteTokenFile() {
29
+ try {
30
+ if (fs.existsSync(TOKEN_FILE)) {
31
+ fs.accessSync(TOKEN_FILE, fs.constants.W_OK);
32
+ return true;
33
+ }
34
+ fs.accessSync(require('path').dirname(TOKEN_FILE), fs.constants.W_OK);
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ module.exports = { run };
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { getToken } = require('../lib/auth');
6
+ const { getBaseUrl } = require('../lib/config');
7
+ const { request } = require('../lib/http');
8
+ const { success, fail, failFromHttpError } = require('../lib/output');
9
+
10
+ async function run(args, opts) {
11
+ const sub = args[0];
12
+ if (sub !== 'publish') fail('Usage: magic-builder faas publish <file.js>', 'E_INVALID_ARGS');
13
+ args = args.slice(1);
14
+ let code;
15
+
16
+ if (opts.code) {
17
+ code = opts.code;
18
+ } else {
19
+ const file = args[0];
20
+ if (!file) fail('Missing file argument. Usage: magic-builder faas publish <file.js> or --code <string>', 'E_INVALID_ARGS');
21
+ if (!fs.existsSync(file)) fail(`File not found: ${file}`, 'E_NOT_FOUND');
22
+ code = fs.readFileSync(file, 'utf8');
23
+ if (!code.trim()) fail('JS file is empty', 'E_EMPTY_FILE');
24
+ }
25
+
26
+ let token;
27
+ try { token = await getToken(opts); } catch (e) { fail(e.message, 'E_NO_TOKEN', 2); }
28
+
29
+ const baseUrl = getBaseUrl(opts);
30
+ const name = opts.name || generateName(args[0]);
31
+
32
+ const body = { code, name };
33
+ if (opts.id) body.id = opts.id;
34
+
35
+ if (!opts.quiet) process.stderr.write(`Deploying FaaS${opts.name ? ` "${opts.name}"` : ''}... `);
36
+
37
+ let res;
38
+ try {
39
+ res = await request(`${baseUrl}/api/faas`, {
40
+ method: 'POST',
41
+ token,
42
+ body,
43
+ });
44
+ } catch (e) { failFromHttpError(e); }
45
+
46
+ if (res.code !== 0) fail(res.msg || 'FaaS publish failed', 'E_PUBLISH_FAILED');
47
+
48
+ const data = res.data || res;
49
+ const id = data.id || data.record_id;
50
+ const faasUrl = data.faas_url || `/api/faas/${id}`;
51
+
52
+ if (!opts.quiet) process.stderr.write('done\n');
53
+
54
+ const wsBaseUrl = baseUrl.replace(/^https?/, 'wss');
55
+
56
+ success({
57
+ id: data.record_id || id,
58
+ faas_url: `${baseUrl}${faasUrl}`,
59
+ preview_url: `${baseUrl}/r?fid=${id}`,
60
+ wss_url: `${wsBaseUrl}${faasUrl}`,
61
+ }, opts);
62
+ }
63
+
64
+ function generateName(filePath) {
65
+ if (!filePath) return `func_${Date.now().toString(36)}`;
66
+ const base = path.basename(filePath, path.extname(filePath));
67
+ return base.replace(/[^a-zA-Z0-9_]/g, '_').slice(0, 50);
68
+ }
69
+
70
+ module.exports = { run };