@guanwenai/high-tech-project-cli 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,234 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { basename, extname, isAbsolute } from 'node:path';
3
+
4
+ import { CliError } from '../core/errors.mjs';
5
+ import { resolveWithinBase } from '../core/files.mjs';
6
+
7
+ export const PROJECT_CONTEXT = ['gatewayUrl', 'uuid'];
8
+ export const PROJECT_WRITING_INTERNAL_PREFIX = '/api/internal/v1/project-writing/';
9
+
10
+ export function toInternalProjectWritingPath(path) {
11
+ if (typeof path !== 'string' || !path.startsWith('/api/project-writing/')) {
12
+ return path;
13
+ }
14
+ return `${PROJECT_WRITING_INTERNAL_PREFIX}${path.slice('/api/project-writing/'.length)}`;
15
+ }
16
+
17
+ export function objectInput(input) {
18
+ if (input === null || Array.isArray(input) || typeof input !== 'object') {
19
+ throw new CliError('ARGUMENT_INVALID', '命令输入必须是对象');
20
+ }
21
+ return input;
22
+ }
23
+
24
+ export function valueOf(input, snakeName, camelName = snakeName) {
25
+ return input[snakeName] ?? input[camelName];
26
+ }
27
+
28
+ export function positiveId(value, field) {
29
+ if (!Number.isSafeInteger(value) || value <= 0) {
30
+ throw new CliError('ARGUMENT_INVALID', `${field} 必须是正整数`);
31
+ }
32
+ return value;
33
+ }
34
+
35
+ export function optionalPositiveId(input, snakeName, camelName = snakeName) {
36
+ const value = valueOf(input, snakeName, camelName);
37
+ if (value !== undefined) positiveId(value, snakeName);
38
+ return value;
39
+ }
40
+
41
+ export function pageInput(input) {
42
+ const value = objectInput(input);
43
+ const pageNumber = valueOf(value, 'page_number', 'pageNumber');
44
+ const pageSize = valueOf(value, 'page_size', 'pageSize');
45
+ if (pageNumber !== undefined) positiveId(pageNumber, 'page_number');
46
+ if (pageSize !== undefined) positiveId(pageSize, 'page_size');
47
+ return value;
48
+ }
49
+
50
+ export function itemsInput(input) {
51
+ const value = objectInput(input);
52
+ if (!Array.isArray(value.items) || value.items.length === 0
53
+ || value.items.some((item) => item === null || Array.isArray(item) || typeof item !== 'object')) {
54
+ throw new CliError('ARGUMENT_INVALID', 'items 必须是非空对象数组');
55
+ }
56
+ return value;
57
+ }
58
+
59
+ export function idsInput(input) {
60
+ const value = objectInput(input);
61
+ const ids = value.ids;
62
+ if (!Array.isArray(ids) || ids.length === 0
63
+ || ids.some((id) => !Number.isSafeInteger(id) || id <= 0)) {
64
+ throw new CliError('ARGUMENT_INVALID', 'ids 必须是非空正整数数组');
65
+ }
66
+ return value;
67
+ }
68
+
69
+ export function contentInput(input) {
70
+ const value = objectInput(input);
71
+ const content = valueOf(value, 'content');
72
+ if (typeof content !== 'string' || !content.trim()) {
73
+ throw new CliError('ARGUMENT_INVALID', 'content 不能为空');
74
+ }
75
+ return value;
76
+ }
77
+
78
+ export function relativePathInput(value, field) {
79
+ if (typeof value !== 'string' || !value.trim() || isAbsolute(value)
80
+ || value.split(/[\\/]/u).includes('..')) {
81
+ throw new CliError('ARGUMENT_INVALID', `${field} 必须是工作目录内的相对路径`);
82
+ }
83
+ return value;
84
+ }
85
+
86
+ export function requiredString(value, field) {
87
+ if (typeof value !== 'string' || !value.trim()) {
88
+ throw new CliError('ARGUMENT_INVALID', `${field} 不能为空`);
89
+ }
90
+ return value.trim();
91
+ }
92
+
93
+ function remotePath(value) {
94
+ return /^https?:\/\//iu.test(value) || value.startsWith('/');
95
+ }
96
+
97
+ function uploadedPath(uploaded) {
98
+ const path = typeof uploaded === 'string'
99
+ ? uploaded
100
+ : uploaded?.filePath ?? uploaded?.url ?? uploaded?.path;
101
+ if (typeof path !== 'string' || !path.trim()) {
102
+ throw new CliError('BUSINESS_ERROR', '文件上传未返回 filePath');
103
+ }
104
+ return path;
105
+ }
106
+
107
+ export async function resolveFileReference({
108
+ client, context, baseDir, filePath, fileName, fileSize,
109
+ }) {
110
+ const requestedPath = requiredString(filePath, 'file_path');
111
+ const requestedName = requiredString(fileName || basename(requestedPath), 'file_name');
112
+ if (remotePath(requestedPath)) {
113
+ return { fileName: requestedName, filePath: requestedPath, fileSize };
114
+ }
115
+ if (typeof baseDir !== 'string' || !baseDir) {
116
+ throw new CliError('CONFIG_MISSING', '文件操作缺少 baseDir');
117
+ }
118
+ const resolvedPath = resolveWithinBase(requestedPath, baseDir);
119
+ let content;
120
+ try {
121
+ content = await readFile(resolvedPath);
122
+ } catch {
123
+ throw new CliError('FILE_NOT_FOUND', '文件不存在', {
124
+ relativePath: requestedPath,
125
+ baseDir,
126
+ resolvedPath,
127
+ });
128
+ }
129
+ const actualName = requestedName.includes('.')
130
+ ? requestedName
131
+ : `${requestedName}${extname(resolvedPath)}`;
132
+ const formData = new FormData();
133
+ formData.append('uuid', context.uuid);
134
+ formData.append('fileName', actualName);
135
+ formData.append('file', new Blob([content]), basename(actualName));
136
+ const uploaded = await client.upload({
137
+ path: `${PROJECT_WRITING_INTERNAL_PREFIX}file/workspace-upload`,
138
+ formData,
139
+ });
140
+ return { fileName: actualName, filePath: uploadedPath(uploaded), fileSize: content.length };
141
+ }
142
+
143
+ export async function resolveSmartImportFiles({ client, context, baseDir, fileNames }) {
144
+ const names = [...new Set(fileNames.map((name) => String(name).trim()).filter(Boolean))];
145
+ const files = [];
146
+ const unresolved = [];
147
+ for (const name of names) {
148
+ if (typeof baseDir !== 'string' || !baseDir || /^https?:\/\//iu.test(name) || name.startsWith('/')) {
149
+ unresolved.push(name);
150
+ continue;
151
+ }
152
+ try {
153
+ const reference = await resolveFileReference({
154
+ client,
155
+ context,
156
+ baseDir,
157
+ filePath: name,
158
+ fileName: basename(name),
159
+ });
160
+ files.push(reference);
161
+ } catch (error) {
162
+ if (error?.code === 'FILE_NOT_FOUND' || error?.code === 'FILE_OUTSIDE_BASE') {
163
+ unresolved.push(name);
164
+ } else {
165
+ throw error;
166
+ }
167
+ }
168
+ }
169
+ return { files, unresolved };
170
+ }
171
+
172
+ export function createFileCommand({ name, path, idField, extraFields = [], validate }) {
173
+ return {
174
+ name,
175
+ method: 'POST',
176
+ path,
177
+ idempotent: false,
178
+ multipart: false,
179
+ requiredContext: PROJECT_CONTEXT,
180
+ validate,
181
+ handler: async ({ client, context, input, baseDir }) => {
182
+ const reference = await resolveFileReference({
183
+ client,
184
+ context,
185
+ baseDir,
186
+ filePath: valueOf(input, 'file_path', 'filePath'),
187
+ fileName: valueOf(input, 'file_name', 'fileName'),
188
+ fileSize: input.fileSize,
189
+ });
190
+ const body = {
191
+ ...extraFields.reduce((result, field) => {
192
+ const value = valueOf(input, field.snake, field.camel);
193
+ if (value !== undefined) result[field.camel] = value;
194
+ return result;
195
+ }, {}),
196
+ ...reference,
197
+ uuid: context.uuid,
198
+ };
199
+ if (idField) body[idField] = valueOf(input, idField.replace(/[A-Z]/g, (match) => `_${match.toLowerCase()}`), idField);
200
+ return await client.request({ method: 'POST', path, body, idempotent: false });
201
+ },
202
+ };
203
+ }
204
+
205
+ export function withJavaMethod(command, javaMethod) {
206
+ if (!javaMethod) return command;
207
+ const internalPath = toInternalProjectWritingPath(command.path);
208
+ return {
209
+ ...command,
210
+ path: internalPath,
211
+ javaMethod,
212
+ handler: async ({ client, ...input }) => command.handler({
213
+ ...input,
214
+ client: client && {
215
+ request: (request) => client.request({
216
+ ...request,
217
+ path: toInternalProjectWritingPath(request.path),
218
+ }),
219
+ upload: client.upload
220
+ ? (request) => client.upload({
221
+ ...request,
222
+ path: toInternalProjectWritingPath(request.path),
223
+ })
224
+ : undefined,
225
+ download: client.download
226
+ ? (request) => client.download({
227
+ ...request,
228
+ path: toInternalProjectWritingPath(request.path),
229
+ })
230
+ : undefined,
231
+ },
232
+ }),
233
+ };
234
+ }