@xiaoyuanhao/yunzhi-cli 2.0.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,22 @@
1
+ const { optionalFields, nullableFields, nullable, present } = require('./result.cjs');
2
+ const source = { ...optionalFields({ name: 'string', org_name: 'string', reporter_name: 'string', contributor_name: 'string',
3
+ file_ext: 'string', type: 'string', size_bytes: 'integer', updated_at: 'string', category_name: 'string', labels: ['string'], web_url: 'string' }), file_id: present('string') };
4
+ const detail = { ...optionalFields({ report_id: 'string', status: 'string', source,
5
+ target: optionalFields({ space: 'string', org_name: 'string', library_name: 'string', directory_name: 'string', path: 'string' }),
6
+ remark: 'string', submitted_at: 'string', processed_at: 'string',
7
+ actual: optionalFields({ file_id: 'string', name: 'string', path: 'string', web_url: 'string' }),
8
+ timeline: [optionalFields({ status: 'string', actor_name: 'string', reason: 'string', occurred_at: 'string' })],
9
+ }), approval_preview: present({ ...nullableFields({ category_name: 'string', folder_id: 'string', library_name: 'string', directory_name: 'string', path: 'string' }), labels: ['string'] }) };
10
+ const event = optionalFields({ event: 'string', report_id: 'string', status: 'string', source_name: 'string', reason: 'string' });
11
+ module.exports = {
12
+ errors: {
13
+ report_already_exists: { report_id: 'string', status: 'string', source_name: 'string' },
14
+ report_not_pending: { current_status: 'string' },
15
+ },
16
+ 'report/submit': event, 'report/cancel': event, 'review/reject': event,
17
+ 'report/show': detail, 'review/show': detail,
18
+ 'report/list': { reports: [{ ...optionalFields({ report_id: 'string', source_name: 'string', status: 'string', target_org_name: 'string', target_directory_name: 'string', target_path: 'string', submitted_at: 'string', processed_at: 'string' }), file_id: present('string') }], next_cursor: nullable('string') },
19
+ 'report/history': { source: nullable(source), attempts: [optionalFields({ report_id: 'string', status: 'string', submitted_at: 'string', processed_at: 'string', reason: 'string' })] },
20
+ 'review/list': { reviews: [optionalFields({ report_id: 'string', status: 'string', source_name: 'string', source_file_ext: 'string', category_name: 'string', source_org_name: 'string', reporter_name: 'string', target_library_name: 'string', target_directory_name: 'string', target_path: 'string', submitted_at: 'string', processed_at: 'string' })], next_cursor: nullable('string') },
21
+ 'review/approve': { event: 'string', report_id: 'string', status: 'string', file_id: 'string', name: 'string', path: 'string' },
22
+ };
@@ -0,0 +1,43 @@
1
+ const { invalid } = require('./result.cjs');
2
+ const { options, required, confirm, limit } = require('./arguments.cjs');
3
+ const gateway = require('./gateway.cjs');
4
+
5
+ async function execute(namespace, command, args) {
6
+ let payload, scope = 'school-library:read';
7
+ switch (command) {
8
+ case 'report/submit':
9
+ payload = options(args, ['file_id', 'remark', ...(namespace === 'lesson-prep' ? ['target_folder_id'] : [])], [], [], true);
10
+ required(payload, 'file_id');
11
+ if (payload.remark && ([...payload.remark].length > 500 || payload.remark.includes('\0'))) throw invalid('--remark 最多 500 个字符,且不能包含空字符。');
12
+ scope = 'school-library:write'; break;
13
+ case 'report/list':
14
+ case 'review/list': {
15
+ const review = command === 'review/list';
16
+ const names = ['status', 'limit', 'cursor', ...(review ? ['query'] : [])];
17
+ payload = { status: review ? 'pending' : null, ...(review ? { query: null } : {}), limit: 20, cursor: null,
18
+ ...options(args, names, [], names, true) };
19
+ const statuses = ['pending', 'approved', 'rejected', 'delisted', review ? 'all' : 'cancelled'];
20
+ if (payload.status !== null && !statuses.includes(payload.status)) throw invalid('--status 必须是 ' + statuses.join('、') + '。');
21
+ limit(payload, 100); break;
22
+ }
23
+ case 'report/show': case 'report/history': case 'report/cancel':
24
+ payload = options(args, ['report_id'], [], [], true); required(payload, 'report_id');
25
+ if (command === 'report/cancel') scope = 'school-library:write';
26
+ break;
27
+ case 'review/show': case 'review/approve': case 'review/reject': {
28
+ const reject = command === 'review/reject', write = command !== 'review/show';
29
+ payload = options(args, ['report_id', reject ? 'reason' : 'folder_id'], write ? ['yes'] : [], [], true);
30
+ required(payload, 'report_id');
31
+ if (reject) {
32
+ required(payload, 'reason');
33
+ if ([...payload.reason].length > 500 || payload.reason.includes('\0')) throw invalid('--reason 必须为 1 到 500 个字符,且不能包含空字符。');
34
+ }
35
+ if (write) { confirm(payload); scope = 'school-library:manage'; }
36
+ break;
37
+ }
38
+ default: throw invalid('未知命令。');
39
+ }
40
+ const schemas = require('./report-schemas.cjs');
41
+ return gateway.execute(scope, namespace + '/' + command, payload, schemas[command]);
42
+ }
43
+ module.exports = { execute };
package/lib/result.cjs ADDED
@@ -0,0 +1,73 @@
1
+ const { attach, cleanup } = require('./diagnostics.cjs');
2
+ class Failure extends Error {
3
+ constructor(code, type, subtype, message, extra = {}) {
4
+ super(message);
5
+ this.result = { code, value: { ok: false, error: { type, subtype, message, ...extra } } };
6
+ }
7
+ }
8
+ const success = data => ({ code: 0, value: { ok: true, data } });
9
+ const invalid = message => new Failure(2, 'validation', 'invalid_arguments', message);
10
+ const internal = (step, message, cause) => {
11
+ const failure = new Failure(5, 'internal', 'cli_internal_error', message, { step });
12
+ return cause ? attach(failure, cause) : failure;
13
+ };
14
+ const authRequired = (message, scope = 'school-library:read') => new Failure(3, 'authentication', 'auth_required', message, {
15
+ required_scopes: [scope],
16
+ });
17
+
18
+ // Preserve the primary failure; only attach already-projected public results.
19
+ function cleanupFailure(error, completed, cause) {
20
+ if (!(error instanceof Failure)) return error;
21
+ if (cause) cleanup(error, cause);
22
+ else error.result.value.error.cleanup_failed = true;
23
+ if (completed) {
24
+ error.result.value.error.operation_completed = true;
25
+ error.result.value.data = completed.value.data;
26
+ if (completed.value.request_id) error.result.value.request_id = completed.value.request_id;
27
+ }
28
+ return error;
29
+ }
30
+
31
+ // Only explicitly declared fields may cross the CLI output boundary.
32
+ const required = schema => ({ $required: schema });
33
+ const optional = schema => ({ $optional: schema });
34
+ const nullable = schema => ({ $nullable: schema });
35
+ const present = schema => ({ $present: schema });
36
+ const optionalFields = schema => Object.fromEntries(Object.entries(schema).map(([key, type]) => [key, optional(type)]));
37
+ const nullableFields = schema => Object.fromEntries(Object.entries(schema).map(([key, type]) => [key, nullable(type)]));
38
+ function fields(value, schema, path = 'data') {
39
+ if (schema.$required !== undefined) {
40
+ if (value == null || (schema.$required === 'string' && (typeof value !== 'string' || !value.trim()))) {
41
+ throw new Error(`响应字段 ${path} 缺失或无效。`);
42
+ }
43
+ return fields(value, schema.$required, path);
44
+ }
45
+ if (schema.$present !== undefined) return value == null ? value : fields(value, schema.$present, path);
46
+ if (schema.$optional !== undefined) return value == null ? undefined : fields(value, schema.$optional, path);
47
+ if (schema.$nullable !== undefined) return value == null ? null : fields(value, schema.$nullable, path);
48
+ if (Array.isArray(schema)) {
49
+ if (value == null) return null;
50
+ if (!Array.isArray(value)) throw new Error(`响应字段 ${path} 类型无效。`);
51
+ return value.map((item, index) => fields(item, schema[0], `${path}[${index}]`));
52
+ }
53
+ if (typeof schema === 'string') {
54
+ if (schema === 'integer') {
55
+ if (value == null) return 0;
56
+ if (!Number.isSafeInteger(value)) throw new Error(`响应字段 ${path} 类型无效。`);
57
+ return value;
58
+ }
59
+ if (value == null) return { string: '', number: 0, boolean: false }[schema];
60
+ if (typeof value !== schema) throw new Error(`响应字段 ${path} 类型无效。`);
61
+ return value;
62
+ }
63
+ if (value == null) value = {};
64
+ if (typeof value !== 'object' || Array.isArray(value)) throw new Error(`响应字段 ${path} 类型无效。`);
65
+ const result = {};
66
+ for (const [key, type] of Object.entries(schema)) {
67
+ const projected = fields(value[key], type, `${path}.${key}`);
68
+ if (projected !== undefined) result[key] = projected;
69
+ }
70
+ return result;
71
+ }
72
+
73
+ module.exports = { Failure, success, invalid, internal, authRequired, cleanupFailure, fields, required, optional, nullable, present, optionalFields, nullableFields };
@@ -0,0 +1,24 @@
1
+ const { detail } = require('./diagnostics.cjs');
2
+ const { authRequired, Failure } = require('./result.cjs');
3
+ const { loadConfig, readCredential, expired } = require('./storage.cjs');
4
+
5
+ async function tokenFor(profile) {
6
+ if (!profile) return '';
7
+ try { return await readCredential(profile.credential_ref); }
8
+ catch (error) {
9
+ if (error.code === 'ENOENT') return '';
10
+ throw new Failure(5, 'internal', 'local_storage_error', '无法读取本地凭据。', { step: 'read_credential', detail: detail(error) });
11
+ }
12
+ }
13
+
14
+ async function session(scope) {
15
+ const config = await loadConfig();
16
+ if (!config.current_profile) throw authRequired('本机尚未登录校园号。', scope);
17
+ if (expired(config.current_profile)) throw authRequired('本机校园号登录已过期。', scope);
18
+ const token = await tokenFor(config.current_profile);
19
+ if (!token) throw authRequired('本机登录凭据缺失,无法执行本次业务。', scope);
20
+ return { config, token };
21
+ }
22
+
23
+ function writeScope(namespace) { return namespace === 'school-library' ? 'school-library:manage' : 'school-library:write'; }
24
+ module.exports = { session, tokenFor, writeScope };
@@ -0,0 +1,122 @@
1
+ const fs = require('node:fs/promises');
2
+ const path = require('node:path');
3
+ const os = require('node:os');
4
+ const { randomBytes } = require('node:crypto');
5
+ const { internal, Failure } = require('./result.cjs');
6
+ const { detail, cleanup } = require('./diagnostics.cjs');
7
+
8
+ const root = () => process.env.YUNZHI_CONFIG_DIR?.trim() || path.join(os.homedir(), '.yunzhi');
9
+ async function loadConfig() {
10
+ try {
11
+ const config = JSON.parse(await fs.readFile(path.join(root(), 'config.json'), 'utf8'));
12
+ if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('invalid config');
13
+ return config;
14
+ } catch (error) {
15
+ if (error.code === 'ENOENT') return {};
16
+ throw internal('read_config', '无法读取本地配置。', error);
17
+ }
18
+ }
19
+ async function credentialPath(reference, creating = false) {
20
+ if (!/^cred_[0-9a-f]{32}$/.test(reference)) throw new Error('invalid credential reference');
21
+ const directory = path.join(root(), 'credentials');
22
+ for (const name of [root(), directory]) {
23
+ const info = await fs.lstat(name);
24
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error('invalid credential directory');
25
+ }
26
+ const file = path.join(directory, reference);
27
+ let info;
28
+ try { info = await fs.lstat(file); }
29
+ catch (error) { if (creating && error.code === 'ENOENT') return file; throw error; }
30
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error('invalid credential');
31
+ return file;
32
+ }
33
+ async function readCredential(reference) {
34
+ return fs.readFile(await credentialPath(reference), 'utf8');
35
+ }
36
+ const expired = profile => Boolean(profile?.expires_at && Date.parse(profile.expires_at) <= Date.now());
37
+
38
+ async function ensureDirectory(directory) {
39
+ await fs.mkdir(directory, { recursive: true });
40
+ const info = await fs.lstat(directory);
41
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error('invalid directory');
42
+ }
43
+ function diagnostic(error) {
44
+ return { detail: error.detail || detail(error), ...(error.cleanup_failed ? { cleanup_failed: true } : {}) };
45
+ }
46
+ const storageFailure = (step, message, error) => new Failure(5, 'internal', 'local_storage_error', message, { step, ...diagnostic(error) });
47
+ async function closeFile(handle, primaryError) {
48
+ try { await handle.close(); }
49
+ catch (error) {
50
+ if (!primaryError) throw error;
51
+ primaryError.detail ||= detail(primaryError);
52
+ cleanup(primaryError, error);
53
+ }
54
+ }
55
+ async function clearFile(file) {
56
+ const info = await fs.lstat(file);
57
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error('invalid cleanup target');
58
+ const handle = await fs.open(file, 'r+');
59
+ let primaryError;
60
+ try { await handle.truncate(0); await handle.sync(); }
61
+ catch (error) { primaryError = error; throw error; }
62
+ finally { await closeFile(handle, primaryError); }
63
+ }
64
+ async function removeCredential(reference) {
65
+ let file;
66
+ try { file = await credentialPath(reference); }
67
+ catch (error) { if (error.code === 'ENOENT') return; throw storageFailure('delete_credential', '无法清理本地凭据。', error); }
68
+ try { await clearFile(file); }
69
+ catch (error) { throw storageFailure('delete_credential', '无法清理本地凭据。', error); }
70
+ }
71
+ async function putCredential(token) {
72
+ const reference = 'cred_' + randomBytes(16).toString('hex');
73
+ let file, created = false;
74
+ try {
75
+ await ensureDirectory(root());
76
+ await ensureDirectory(path.join(root(), 'credentials'));
77
+ file = await credentialPath(reference, true);
78
+ const handle = await fs.open(file, 'wx');
79
+ created = true;
80
+ let primaryError;
81
+ try {
82
+ await handle.writeFile(token, 'utf8');
83
+ await handle.sync();
84
+ } catch (error) { primaryError = error; throw error; }
85
+ finally { await closeFile(handle, primaryError); }
86
+ return reference;
87
+ } catch (error) {
88
+ const failure = storageFailure('write_credential', '无法写入本地凭据文件。', error);
89
+ if (created) {
90
+ try { await clearFile(file); }
91
+ catch (cleanupError) { cleanup(failure, cleanupError); }
92
+ }
93
+ throw failure;
94
+ }
95
+ }
96
+ async function saveConfig(config) {
97
+ const destination = path.join(root(), 'config.json');
98
+ let opened = false;
99
+ try {
100
+ const content = JSON.stringify(config, null, 2) + '\n';
101
+ await ensureDirectory(root());
102
+ try {
103
+ const info = await fs.lstat(destination);
104
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error('invalid config target');
105
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
106
+ const handle = await fs.open(destination, 'w');
107
+ opened = true;
108
+ let primaryError;
109
+ try {
110
+ await handle.writeFile(content);
111
+ await handle.sync();
112
+ } catch (error) { primaryError = error; throw error; }
113
+ finally { await closeFile(handle, primaryError); }
114
+ } catch (error) {
115
+ throw new Failure(5, 'internal', 'profile_write_failed', '无法保存本地配置;本次配置写入未完成。', {
116
+ step: 'save_profile', ...diagnostic(error),
117
+ ...(opened ? { local_path: destination, partial_file: true } : {}),
118
+ });
119
+ }
120
+ }
121
+
122
+ module.exports = { root, loadConfig, readCredential, expired, putCredential, removeCredential, saveConfig };
@@ -0,0 +1,46 @@
1
+ const { invalid } = require('./result.cjs');
2
+
3
+ function distance(left, right) {
4
+ let row = Array.from({ length: right.length + 1 }, (_, i) => i);
5
+ for (let i = 0; i < left.length; i++) {
6
+ const next = [i + 1];
7
+ for (let j = 0; j < right.length; j++) next.push(Math.min(next[j] + 1, row[j + 1] + 1, row[j] + (left[i] === right[j] ? 0 : 1)));
8
+ row = next;
9
+ }
10
+ return row[right.length];
11
+ }
12
+ function candidates(input, names, maximum) {
13
+ if (input.length > 100) return [];
14
+ const word = input.replace(/^--/, '');
15
+ return [...new Set(names)].map(name => {
16
+ const value = name.replace(/^--/, '');
17
+ let prefix = 0;
18
+ while (word[prefix] && word[prefix] === value[prefix]) prefix++;
19
+ return { name, prefix, distance: distance(word, value), segment: word.split('-').some(part => part && value.split('-').includes(part)) };
20
+ }).filter(item => item.prefix >= 3 || item.distance <= Math.max(2, Math.floor(word.length / 3)) || item.segment)
21
+ .sort((a, b) => b.prefix - a.prefix || a.distance - b.distance || a.name.localeCompare(b.name))
22
+ .slice(0, maximum).map(item => item.name);
23
+ }
24
+ function unknownOption(input, names) {
25
+ const error = invalid('包含未知参数:' + input);
26
+ error.result.value.error.candidates = candidates(input, names, 3);
27
+ return error;
28
+ }
29
+ function unknownCommand(words, names) {
30
+ let prefix = [];
31
+ for (const word of words) {
32
+ const children = [...new Set(names.filter(name => name.startsWith(prefix.length ? prefix.join(' ') + ' ' : ''))
33
+ .map(name => name.split(' ')[prefix.length]))];
34
+ if (!children.includes(word)) {
35
+ const error = invalid('未知命令:' + [...prefix, word].join(' ') + '。');
36
+ const help = prefix.length ? prefix.join(' ') + ' --help' : '--help';
37
+ Object.assign(error.result.value.error, { candidates: candidates(word, children, 6).map(name => [...prefix, name].join(' ')), hint: '请核对候选;使用 ' + help + ' 查看可用命令,不自动执行候选。' });
38
+ return error;
39
+ }
40
+ prefix.push(word);
41
+ }
42
+ const error = invalid('命令不完整。');
43
+ Object.assign(error.result.value.error, { candidates: [], hint: '使用 ' + (prefix.length ? prefix.join(' ') + ' --help' : '--help') + ' 查看可用命令。' });
44
+ return error;
45
+ }
46
+ module.exports = { unknownOption, unknownCommand };
package/lib/upload.cjs ADDED
@@ -0,0 +1,76 @@
1
+ const { attach } = require('./diagnostics.cjs');
2
+ const fs = require('node:fs/promises');
3
+ const path = require('node:path');
4
+ const { Failure, nullableFields, cleanupFailure } = require('./result.cjs');
5
+ const { options, required } = require('./arguments.cjs');
6
+ const { session, writeScope } = require('./session.cjs');
7
+ const { request, publicResult } = require('./gateway.cjs');
8
+ const extensions = new Set('pdf doc docx ppt pptx xls xlsx jpg jpeg png gif mp4 avi mov mp3 wav txt m3u8 flv mpd'.split(' '));
9
+
10
+ async function upload(namespace, args) {
11
+ const parsed = options(args, ['file', 'folder_id', 'title', 'category_id', 'label_id'], [], ['file', 'folder_id', 'category_id', 'label_id']);
12
+ required(parsed, 'file', 'folder_id');
13
+ let handle, result, primaryError;
14
+ let stage = 'local_file';
15
+ try { handle = await fs.open(parsed.file, 'r'); }
16
+ catch (error) { throw attach(new Failure(2, 'validation', 'invalid_local_file', '本地文件不可读取,上传尚未开始。'), error); }
17
+ try {
18
+ let info;
19
+ try { info = await handle.stat(); if (!info.isFile()) throw new Error(); }
20
+ catch (error) { throw attach(new Failure(2, 'validation', 'invalid_local_file', '本地路径不存在或不是普通文件,上传尚未开始。'), error); }
21
+ if (info.size === 0) throw new Failure(2, 'validation', 'invalid_local_file', '本地文件为空,上传尚未开始。');
22
+ const name = path.basename(parsed.file), extension = path.extname(name);
23
+ if (!extensions.has(extension.slice(1).toLowerCase())) throw new Failure(2, 'validation', 'unsupported_file_type', '不支持该文件格式,上传尚未开始。');
24
+ if (info.size >= 500 * 1024 * 1024) throw new Failure(2, 'validation', 'file_too_large', '文件必须小于 500 MiB,上传尚未开始。');
25
+ const base = name.slice(0, name.length - extension.length);
26
+ const title = parsed.title || (base.trim() ? base : name);
27
+ const { config, token } = await session(writeScope(namespace));
28
+ stage = 'upload_authorization';
29
+ const prepared = await request(config, namespace + '/upload/prepare', token, { folder_id: parsed.folder_id, file_name: name, size_bytes: info.size });
30
+ let grant;
31
+ try {
32
+ const value = prepared.data;
33
+ const text = input => { if (typeof input !== 'string' || !input.trim()) throw new Error(); return input.trim(); };
34
+ grant = { object_key: text(value.object_key), bucket: text(value.bucket), region: text(value.region),
35
+ secret_id: text(value.credentials.secret_id), secret_key: text(value.credentials.secret_key), session_token: text(value.credentials.session_token) };
36
+ const expires = text(value.credentials.expires_at);
37
+ if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(grant.bucket) || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(grant.region) ||
38
+ !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(expires) || !Number.isFinite(Date.parse(expires)) || Date.parse(expires) <= Date.now()) throw new Error();
39
+ } catch (error) { throw attach(new Failure(1, 'api', 'invalid_response', 'Agent Gateway 返回了无效上传授权。'), error); }
40
+ stage = 'file_transfer';
41
+ try { await require('./cos-upload.cjs').put(handle, info.size, grant); }
42
+ catch (error) { throw attach(new Failure(4, 'network', 'upload_failed', '文件传输结果无法确认,业务资源创建尚未开始。'), error); }
43
+ stage = 'resource_creation';
44
+ const completed = await request(config, namespace + '/upload/complete', token, {
45
+ folder_id: parsed.folder_id, object_key: grant.object_key, title, category_id: parsed.category_id || null, label_ids: parsed.label_ids || [],
46
+ });
47
+ const output = publicResult(completed, nullableFields({ type: 'string', file_id: 'string', kind: 'string', name: 'string', path: 'string',
48
+ file_ext: 'string', file_size: 'integer', updated_at: 'string', indexing: 'string',
49
+ ...(namespace === 'school-library' ? { space: 'string', web_url: 'string' } : {}),
50
+ }));
51
+ if (typeof output.value.data.file_id !== 'string' || !output.value.data.file_id.trim()) {
52
+ const failure = new Failure(1, 'api', 'invalid_response', '响应字段 data.file_id 缺失或无效,暂时无法确认业务资源创建结果。');
53
+ if (output.value.request_id) failure.result.value.request_id = output.value.request_id;
54
+ throw failure;
55
+ }
56
+ result = output;
57
+ return result;
58
+ } catch (error) {
59
+ if (error instanceof Failure) {
60
+ const failure = error.result.value.error;
61
+ failure.upload_stage = stage;
62
+ if (stage === 'resource_creation') {
63
+ failure.file_transferred = true;
64
+ failure.message = '文件传输已完成;业务资源创建阶段:' + failure.message;
65
+ } else if (stage === 'upload_authorization') failure.message = '取得上传授权阶段失败,文件传输尚未开始。' + failure.message;
66
+ }
67
+ primaryError = error;
68
+ throw error;
69
+ } finally {
70
+ try { await handle.close(); }
71
+ catch (error) {
72
+ throw cleanupFailure(primaryError || new Failure(5, 'local', 'storage_error', '无法关闭上传源文件。', { step: 'close_upload_source' }), result, error);
73
+ }
74
+ }
75
+ }
76
+ module.exports = { upload };
@@ -0,0 +1,3 @@
1
+ // The package manifest owns the CLI release identity.
2
+ const { version } = require('../package.json');
3
+ module.exports = { version };
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@xiaoyuanhao/yunzhi-cli",
3
+ "version": "2.0.0",
4
+ "description": "Yunzhi Campus CLI for lesson preparation and school library workflows",
5
+ "bin": {
6
+ "yunzhi-cli": "yunzhi-cli.cjs"
7
+ },
8
+ "engines": {
9
+ "node": ">=22"
10
+ },
11
+ "files": [
12
+ "yunzhi-cli.cjs",
13
+ "lib/"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ }
18
+ }
package/yunzhi-cli.cjs ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ const { detail } = require('./lib/diagnostics.cjs');
3
+ const { run } = require('./lib/cli.cjs');
4
+
5
+ run(process.argv.slice(2)).then(({ code, value, text }) => {
6
+ (code ? process.stderr : process.stdout).write(text === undefined ? JSON.stringify(value) + '\n' : text);
7
+ process.exitCode = code;
8
+ }).catch(error => {
9
+ process.stderr.write(JSON.stringify({ ok: false, error: {
10
+ type: 'internal', subtype: 'cli_internal_error', message: 'CLI 执行失败。', detail: detail(error),
11
+ } }) + '\n');
12
+ process.exitCode = 5;
13
+ });