magic-builder 0.1.1 → 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 ADDED
@@ -0,0 +1,180 @@
1
+ # magic-builder
2
+
3
+ CLI for Magic Builder (妙笔): publish HTML pages, deploy FaaS functions, upload files, generate links, and install the Magic Builder Codex skill.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g magic-builder
9
+ ```
10
+
11
+ With Bun:
12
+
13
+ ```bash
14
+ bun add -g magic-builder
15
+ ```
16
+
17
+ The package provides three equivalent command names:
18
+
19
+ ```bash
20
+ magic-builder --help
21
+ magic-cli --help
22
+ miaobi --help
23
+ ```
24
+
25
+ You can also run it without installing:
26
+
27
+ ```bash
28
+ npx magic-builder help
29
+ bunx magic-builder help
30
+ ```
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
+
41
+ ## Authentication
42
+
43
+ Most write operations need a Magic developer token.
44
+
45
+ ```bash
46
+ magic-builder auth login
47
+ ```
48
+
49
+ Manual token setup:
50
+
51
+ ```bash
52
+ magic-builder auth set <token>
53
+ magic-builder auth show
54
+ ```
55
+
56
+ Token lookup order:
57
+
58
+ 1. `--token`
59
+ 2. `MAGIC_TOKEN`
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
+ ```
76
+
77
+ ## Common Commands
78
+
79
+ Publish an HTML page:
80
+
81
+ ```bash
82
+ magic-builder page publish app.html --title "Dashboard"
83
+ ```
84
+
85
+ List or export pages:
86
+
87
+ ```bash
88
+ magic-builder page list --scope mine
89
+ magic-builder page list --scope public --title demo
90
+ magic-builder page export --id <id> --out app.html
91
+ ```
92
+
93
+ Delete a page:
94
+
95
+ ```bash
96
+ magic-builder page delete --id <id>
97
+ ```
98
+
99
+ Deploy a FaaS function:
100
+
101
+ ```bash
102
+ magic-builder faas publish handler.js --name report-api
103
+ ```
104
+
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:
114
+
115
+ ```bash
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>
120
+ ```
121
+
122
+ Generate a Magic link:
123
+
124
+ ```bash
125
+ magic-builder link create --title "Weekly Report"
126
+ magic-builder link create --fid <faas-id>
127
+ ```
128
+
129
+ Create or append a Feishu document HTML Box:
130
+
131
+ ```bash
132
+ magic-builder doc create --html app.html --title "Demo"
133
+ magic-builder doc append --html app.html --doc-token <docx-token>
134
+ ```
135
+
136
+ Install or update the Magic Builder Codex skill:
137
+
138
+ ```bash
139
+ magic-builder skill install
140
+ magic-builder skill check-update
141
+ magic-builder skill update
142
+ ```
143
+
144
+ Check local environment:
145
+
146
+ ```bash
147
+ magic-builder version
148
+ magic-builder doctor
149
+ magic-builder config get
150
+ ```
151
+
152
+ ## Help
153
+
154
+ All of these forms are supported:
155
+
156
+ ```bash
157
+ magic-builder help
158
+ magic-builder help page
159
+ magic-builder page help
160
+ magic-builder page publish help
161
+ magic-builder man
162
+ ```
163
+
164
+ `--help`, `-h`, and `--man` are also supported.
165
+
166
+ ## Global Options
167
+
168
+ ```text
169
+ --base-url, --magic-base-url <url> Magic service base URL
170
+ --token <token> Auth token override
171
+ --format json|table|plain Output format, default json
172
+ --quiet, -q Suppress progress messages
173
+ --version, -v Show version
174
+ ```
175
+
176
+ Default Magic service:
177
+
178
+ ```text
179
+ https://magic.solutionsuite.cn
180
+ ```
@@ -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
- asset: require('../src/commands/asset'),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magic-builder",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "CLI for Magic Builder — publish pages, functions, and files",
5
5
  "bin": {
6
6
  "magic-builder": "./bin/magic-builder.js",
@@ -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
  }
@@ -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: canWriteTokenFile(), detail: TOKEN_FILE });
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 canWriteTokenFile() {
30
+ function canWritePath(targetPath) {
29
31
  try {
30
- if (fs.existsSync(TOKEN_FILE)) {
31
- fs.accessSync(TOKEN_FILE, fs.constants.W_OK);
32
+ if (fs.existsSync(targetPath)) {
33
+ fs.accessSync(targetPath, fs.constants.W_OK);
32
34
  return true;
33
35
  }
34
- fs.accessSync(require('path').dirname(TOKEN_FILE), fs.constants.W_OK);
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;
@@ -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 !== 'publish') fail('Usage: magic-builder faas publish <file.js>', 'E_INVALID_ARGS');
13
- args = args.slice(1);
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 };
@@ -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
- fail('Usage: magic-builder page <publish|list|export>', 'E_INVALID_ARGS');
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
- asset: require('./commands/asset'),
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(os.homedir(), '.magic-token');
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 APPS_CONFIG_FILE = '.magic-apps.json';
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(process.cwd(), APPS_CONFIG_FILE);
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 export Magic pages
19
- faas Publish Magic FaaS functions
20
- asset Upload files to TOS
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 asset upload logo.png
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
- asset upload <file> [--key <key>] [--content-type <mime>]
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 environment variable, ~/.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
- asset: `@HELP magic-builder/asset
163
+ file: `@HELP magic-builder/file
138
164
 
139
165
  SYNTAX:
140
- magic-builder asset upload <file> [--key <key>] [--content-type <mime>]
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
 
@@ -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
- return { url, key: signRes.data.key, size: buf.length };
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
- body: { uploadId, key: tosKey, parts },
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 { url: completeRes.data?.url || url, key: tosKey, size: stat.size };
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) {
@@ -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 };