xcode-cli 1.0.5
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/LICENSE +21 -0
- package/README.md +151 -0
- package/bin/xcode-cli +19 -0
- package/bin/xcode-cli-ctl +19 -0
- package/package.json +41 -0
- package/skills/xcode-cli/SKILL.md +138 -0
- package/src/mcpbridge.ts +1624 -0
- package/src/xcode-ctl.ts +138 -0
- package/src/xcode-issues.ts +165 -0
- package/src/xcode-mcp.ts +483 -0
- package/src/xcode-output.ts +431 -0
- package/src/xcode-preview.ts +55 -0
- package/src/xcode-service.ts +278 -0
- package/src/xcode-skill.ts +52 -0
- package/src/xcode-test.ts +115 -0
- package/src/xcode-tree.ts +59 -0
- package/src/xcode-types.ts +28 -0
- package/src/xcode.ts +785 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import type { CallResult } from 'mcporter';
|
|
2
|
+
import type { CommonOpts } from './xcode-types.ts';
|
|
3
|
+
|
|
4
|
+
export function unwrapResult(result: CallResult): unknown {
|
|
5
|
+
const structured = result.structuredContent();
|
|
6
|
+
if (structured !== undefined && structured !== null) {
|
|
7
|
+
return structured;
|
|
8
|
+
}
|
|
9
|
+
const json = result.json();
|
|
10
|
+
if (json !== null) {
|
|
11
|
+
return json;
|
|
12
|
+
}
|
|
13
|
+
return result.raw;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function printResult(result: CallResult, output: CommonOpts['output']) {
|
|
17
|
+
const value = unwrapResult(result);
|
|
18
|
+
if (output === 'json') {
|
|
19
|
+
console.log(JSON.stringify(value, null, 2));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log(formatReadableOutput(value));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatReadableOutput(value: unknown): string {
|
|
26
|
+
if (value === null || value === undefined) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === 'string') {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
if (value.length === 0) {
|
|
34
|
+
return '(no items)';
|
|
35
|
+
}
|
|
36
|
+
return value.map((item) => `- ${formatListItem(item)}`).join('\n');
|
|
37
|
+
}
|
|
38
|
+
if (typeof value !== 'object') {
|
|
39
|
+
return String(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const record = value as Record<string, unknown>;
|
|
43
|
+
|
|
44
|
+
if (isTestRunPayload(record)) {
|
|
45
|
+
return formatTestRunOutput(record);
|
|
46
|
+
}
|
|
47
|
+
if (isBuildLogPayload(record)) {
|
|
48
|
+
return formatBuildLogOutput(record);
|
|
49
|
+
}
|
|
50
|
+
if (isDocumentationPayload(record)) {
|
|
51
|
+
return formatDocumentationOutput(record);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (record.type === 'error' && typeof record.data === 'string') {
|
|
55
|
+
return `Error: ${record.data}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (typeof record.previewSnapshotPath === 'string') {
|
|
59
|
+
return record.previewSnapshotPath;
|
|
60
|
+
}
|
|
61
|
+
if (typeof record.executionResults === 'string') {
|
|
62
|
+
return record.executionResults.trimEnd();
|
|
63
|
+
}
|
|
64
|
+
if (typeof record.buildResult === 'string') {
|
|
65
|
+
const errors = Array.isArray(record.errors) ? record.errors : [];
|
|
66
|
+
if (errors.length === 0) {
|
|
67
|
+
return record.buildResult;
|
|
68
|
+
}
|
|
69
|
+
return `${record.buildResult}\nErrors:\n${errors.map((entry) => `- ${formatListItem(entry)}`).join('\n')}`;
|
|
70
|
+
}
|
|
71
|
+
if (typeof record.content === 'string' && typeof record.filePath === 'string') {
|
|
72
|
+
const header = `${record.filePath}`;
|
|
73
|
+
const sep = '-'.repeat(header.length);
|
|
74
|
+
return `${header}\n${sep}\n${record.content}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (Array.isArray(record.issues)) {
|
|
78
|
+
return formatNamedList('Issues', record.issues);
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(record.documents)) {
|
|
81
|
+
return formatNamedList('Documents', record.documents);
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(record.tests)) {
|
|
84
|
+
return formatNamedList('Tests', record.tests);
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(record.items)) {
|
|
87
|
+
return formatNamedList('Items', record.items);
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(record.matches)) {
|
|
90
|
+
return formatNamedList('Matches', record.matches);
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(record.results)) {
|
|
93
|
+
return formatNamedList('Results', record.results);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return formatObject(record);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isTestRunPayload(record: Record<string, unknown>): boolean {
|
|
100
|
+
return Boolean(
|
|
101
|
+
record &&
|
|
102
|
+
typeof record.summary === 'string' &&
|
|
103
|
+
Array.isArray(record.results) &&
|
|
104
|
+
record.counts &&
|
|
105
|
+
typeof record.counts === 'object',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isBuildLogPayload(record: Record<string, unknown>): boolean {
|
|
110
|
+
return Array.isArray(record.buildLogEntries) && typeof record.buildResult === 'string';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isDocumentationPayload(record: Record<string, unknown>): boolean {
|
|
114
|
+
return Array.isArray(record.documents);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatDocumentationOutput(record: Record<string, unknown>): string {
|
|
118
|
+
const lines: string[] = [];
|
|
119
|
+
for (const [key, value] of Object.entries(record)) {
|
|
120
|
+
if (key === 'documents' || value === undefined) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (Array.isArray(value)) {
|
|
124
|
+
if (value.length === 0) {
|
|
125
|
+
lines.push(`${key}: []`);
|
|
126
|
+
} else {
|
|
127
|
+
lines.push(`${key}:`);
|
|
128
|
+
for (const item of value) {
|
|
129
|
+
lines.push(` - ${formatListItem(item)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (value && typeof value === 'object') {
|
|
135
|
+
lines.push(`${key}:`);
|
|
136
|
+
const nested = formatObject(value as Record<string, unknown>)
|
|
137
|
+
.split('\n')
|
|
138
|
+
.map((line) => ` ${line}`);
|
|
139
|
+
lines.push(...nested);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
lines.push(`${key}: ${String(value)}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const documents = (Array.isArray(record.documents) ? record.documents : []).filter(
|
|
146
|
+
(item): item is Record<string, unknown> => Boolean(item) && typeof item === 'object',
|
|
147
|
+
);
|
|
148
|
+
if (lines.length > 0) {
|
|
149
|
+
lines.push('');
|
|
150
|
+
}
|
|
151
|
+
if (documents.length === 0) {
|
|
152
|
+
lines.push('Documents: none');
|
|
153
|
+
return lines.join('\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
lines.push(`Documents (${documents.length})`);
|
|
157
|
+
for (let index = 0; index < documents.length; index += 1) {
|
|
158
|
+
const doc = documents[index];
|
|
159
|
+
const title =
|
|
160
|
+
firstString(doc, ['title', 'displayName', 'name']) ??
|
|
161
|
+
firstString(doc, ['path', 'uri']) ??
|
|
162
|
+
`Document ${index + 1}`;
|
|
163
|
+
lines.push('');
|
|
164
|
+
lines.push(`[${index + 1}] ${title}`);
|
|
165
|
+
const details = formatObject(doc)
|
|
166
|
+
.split('\n')
|
|
167
|
+
.map((line) => ` ${line}`);
|
|
168
|
+
lines.push(...details);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (record.truncated === true) {
|
|
172
|
+
lines.push('');
|
|
173
|
+
lines.push('note: results were truncated by MCP');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return lines.join('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function formatBuildLogOutput(record: Record<string, unknown>): string {
|
|
180
|
+
const lines: string[] = [];
|
|
181
|
+
const buildResult = typeof record.buildResult === 'string' ? record.buildResult : undefined;
|
|
182
|
+
const buildIsRunning = typeof record.buildIsRunning === 'boolean' ? record.buildIsRunning : undefined;
|
|
183
|
+
const fullLogPath = typeof record.fullLogPath === 'string' ? record.fullLogPath : undefined;
|
|
184
|
+
const totalFound =
|
|
185
|
+
typeof record.totalFound === 'number' && Number.isFinite(record.totalFound)
|
|
186
|
+
? record.totalFound
|
|
187
|
+
: undefined;
|
|
188
|
+
const truncated = record.truncated === true;
|
|
189
|
+
|
|
190
|
+
if (buildResult) {
|
|
191
|
+
lines.push(buildResult);
|
|
192
|
+
}
|
|
193
|
+
if (buildIsRunning !== undefined) {
|
|
194
|
+
lines.push(`build running: ${buildIsRunning ? 'yes' : 'no'}`);
|
|
195
|
+
}
|
|
196
|
+
if (fullLogPath) {
|
|
197
|
+
lines.push(`full log path: ${fullLogPath}`);
|
|
198
|
+
}
|
|
199
|
+
if (totalFound !== undefined) {
|
|
200
|
+
lines.push(`matching build entries: ${totalFound}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const entries = (Array.isArray(record.buildLogEntries) ? record.buildLogEntries : []).filter(
|
|
204
|
+
(item): item is Record<string, unknown> => Boolean(item) && typeof item === 'object',
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (entries.length === 0) {
|
|
208
|
+
if (truncated) {
|
|
209
|
+
lines.push('note: results were truncated by MCP');
|
|
210
|
+
}
|
|
211
|
+
return lines.join('\n');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
lines.push('');
|
|
215
|
+
lines.push(`build log entries (${entries.length}):`);
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
const task = typeof entry.buildTask === 'string' ? entry.buildTask : '<unknown task>';
|
|
218
|
+
lines.push(`- task: ${task}`);
|
|
219
|
+
|
|
220
|
+
const issues = (Array.isArray(entry.emittedIssues) ? entry.emittedIssues : []).filter(
|
|
221
|
+
(item): item is Record<string, unknown> => Boolean(item) && typeof item === 'object',
|
|
222
|
+
);
|
|
223
|
+
if (issues.length === 0) {
|
|
224
|
+
lines.push(' issues: none');
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
lines.push(` issues (${issues.length}):`);
|
|
229
|
+
for (const issue of issues) {
|
|
230
|
+
const severity = typeof issue.severity === 'string' ? issue.severity : 'issue';
|
|
231
|
+
const path = typeof issue.path === 'string' ? issue.path : '<unknown path>';
|
|
232
|
+
const line = typeof issue.line === 'number' && Number.isFinite(issue.line) ? issue.line : undefined;
|
|
233
|
+
const message = typeof issue.message === 'string' ? issue.message : '<no message>';
|
|
234
|
+
|
|
235
|
+
lines.push(` - [${severity}] ${line ? `${path}:${line}` : path}`);
|
|
236
|
+
lines.push(` ${message}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (truncated) {
|
|
241
|
+
lines.push('');
|
|
242
|
+
lines.push('note: results were truncated by MCP');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return lines.join('\n');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function formatTestRunOutput(record: Record<string, unknown>): string {
|
|
249
|
+
const lines: string[] = [];
|
|
250
|
+
const summary = typeof record.summary === 'string' ? record.summary : undefined;
|
|
251
|
+
const schemeName = typeof record.schemeName === 'string' ? record.schemeName : undefined;
|
|
252
|
+
const activeTestPlanName =
|
|
253
|
+
typeof record.activeTestPlanName === 'string' ? record.activeTestPlanName : undefined;
|
|
254
|
+
const fullSummaryPath =
|
|
255
|
+
typeof record.fullSummaryPath === 'string' ? record.fullSummaryPath : undefined;
|
|
256
|
+
const truncated = record.truncated === true;
|
|
257
|
+
|
|
258
|
+
if (summary) {
|
|
259
|
+
lines.push(summary);
|
|
260
|
+
}
|
|
261
|
+
if (schemeName) {
|
|
262
|
+
lines.push(`scheme: ${schemeName}`);
|
|
263
|
+
}
|
|
264
|
+
if (activeTestPlanName) {
|
|
265
|
+
lines.push(`test plan: ${activeTestPlanName}`);
|
|
266
|
+
}
|
|
267
|
+
if (fullSummaryPath) {
|
|
268
|
+
lines.push(`full summary: ${fullSummaryPath}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const counts = record.counts as Record<string, unknown>;
|
|
272
|
+
lines.push(
|
|
273
|
+
`counts: total=${toInt(counts.total)} passed=${toInt(counts.passed)} failed=${toInt(counts.failed)} skipped=${toInt(counts.skipped)} expectedFailures=${toInt(counts.expectedFailures)} notRun=${toInt(counts.notRun)}`,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const results = (Array.isArray(record.results) ? record.results : []).filter(
|
|
277
|
+
(item): item is Record<string, unknown> => Boolean(item) && typeof item === 'object',
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
if (results.length === 0) {
|
|
281
|
+
return lines.join('\n');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
lines.push('');
|
|
285
|
+
lines.push(`results (${results.length}):`);
|
|
286
|
+
for (const entry of results) {
|
|
287
|
+
const identifier = typeof entry.identifier === 'string' ? entry.identifier : undefined;
|
|
288
|
+
const displayName = typeof entry.displayName === 'string' ? entry.displayName : undefined;
|
|
289
|
+
const targetName = typeof entry.targetName === 'string' ? entry.targetName : undefined;
|
|
290
|
+
const state = typeof entry.state === 'string' ? entry.state : 'Unknown';
|
|
291
|
+
const label = identifier ?? displayName ?? '<unknown test>';
|
|
292
|
+
|
|
293
|
+
lines.push(`- [${state}] ${label}`);
|
|
294
|
+
if (targetName) {
|
|
295
|
+
lines.push(` target: ${targetName}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const errorMessages = Array.isArray(entry.errorMessages)
|
|
299
|
+
? entry.errorMessages.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
|
300
|
+
: [];
|
|
301
|
+
|
|
302
|
+
if (errorMessages.length > 0) {
|
|
303
|
+
lines.push(' errors:');
|
|
304
|
+
for (const message of errorMessages) {
|
|
305
|
+
lines.push(` - ${message}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (truncated) {
|
|
311
|
+
lines.push('');
|
|
312
|
+
lines.push('note: results were truncated by MCP');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return lines.join('\n');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function toInt(value: unknown): number {
|
|
319
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
320
|
+
return value;
|
|
321
|
+
}
|
|
322
|
+
if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {
|
|
323
|
+
return Number(value);
|
|
324
|
+
}
|
|
325
|
+
return 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function formatNamedList(title: string, items: unknown[]): string {
|
|
329
|
+
if (items.length === 0) {
|
|
330
|
+
return `${title}: none`;
|
|
331
|
+
}
|
|
332
|
+
return `${title} (${items.length})\n${items.map((item) => `- ${formatListItem(item)}`).join('\n')}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function formatListItem(item: unknown): string {
|
|
336
|
+
if (item === null || item === undefined) {
|
|
337
|
+
return '';
|
|
338
|
+
}
|
|
339
|
+
if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') {
|
|
340
|
+
return String(item);
|
|
341
|
+
}
|
|
342
|
+
if (Array.isArray(item)) {
|
|
343
|
+
return item.map((entry) => formatListItem(entry)).join(', ');
|
|
344
|
+
}
|
|
345
|
+
if (typeof item !== 'object') {
|
|
346
|
+
return String(item);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const record = item as Record<string, unknown>;
|
|
350
|
+
const pathValue = firstString(record, ['path', 'filePath', 'file', 'uri']);
|
|
351
|
+
const titleValue = firstString(record, ['title', 'displayName', 'identifier', 'name']);
|
|
352
|
+
const messageValue = firstString(record, ['message', 'contents', 'summary']);
|
|
353
|
+
const severity = firstString(record, ['severity', 'type']);
|
|
354
|
+
const line = firstNumber(record, ['line', 'lineNumber']);
|
|
355
|
+
const score = firstNumber(record, ['score']);
|
|
356
|
+
|
|
357
|
+
const parts: string[] = [];
|
|
358
|
+
if (severity) {
|
|
359
|
+
parts.push(`[${severity}]`);
|
|
360
|
+
}
|
|
361
|
+
if (titleValue) {
|
|
362
|
+
parts.push(titleValue);
|
|
363
|
+
}
|
|
364
|
+
if (pathValue) {
|
|
365
|
+
parts.push(line ? `${pathValue}:${line}` : pathValue);
|
|
366
|
+
}
|
|
367
|
+
if (typeof score === 'number') {
|
|
368
|
+
parts.push(`score=${score.toFixed(3)}`);
|
|
369
|
+
}
|
|
370
|
+
if (messageValue && !titleValue) {
|
|
371
|
+
parts.push(messageValue);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (parts.length > 0) {
|
|
375
|
+
return parts.join(' ');
|
|
376
|
+
}
|
|
377
|
+
return formatObject(record).replace(/\n/g, '; ');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function formatObject(record: Record<string, unknown>): string {
|
|
381
|
+
const lines: string[] = [];
|
|
382
|
+
for (const [key, value] of Object.entries(record)) {
|
|
383
|
+
if (value === undefined) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (Array.isArray(value)) {
|
|
387
|
+
if (value.length === 0) {
|
|
388
|
+
lines.push(`${key}: []`);
|
|
389
|
+
} else {
|
|
390
|
+
lines.push(`${key}:`);
|
|
391
|
+
for (const item of value) {
|
|
392
|
+
lines.push(` - ${formatListItem(item)}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (value && typeof value === 'object') {
|
|
398
|
+
lines.push(`${key}:`);
|
|
399
|
+
const nested = formatObject(value as Record<string, unknown>)
|
|
400
|
+
.split('\n')
|
|
401
|
+
.map((line) => ` ${line}`);
|
|
402
|
+
lines.push(...nested);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
lines.push(`${key}: ${String(value)}`);
|
|
406
|
+
}
|
|
407
|
+
return lines.join('\n');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function firstString(record: Record<string, unknown>, keys: string[]): string | undefined {
|
|
411
|
+
for (const key of keys) {
|
|
412
|
+
const value = record[key];
|
|
413
|
+
if (typeof value === 'string' && value.trim()) {
|
|
414
|
+
return value;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function firstNumber(record: Record<string, unknown>, keys: string[]): number | undefined {
|
|
421
|
+
for (const key of keys) {
|
|
422
|
+
const value = record[key];
|
|
423
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
424
|
+
return value;
|
|
425
|
+
}
|
|
426
|
+
if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {
|
|
427
|
+
return Number(value);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function findPreviewPath(value: unknown): string | undefined {
|
|
5
|
+
const queue: unknown[] = [value];
|
|
6
|
+
while (queue.length > 0) {
|
|
7
|
+
const current = queue.shift();
|
|
8
|
+
if (!current || typeof current !== 'object') {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(current)) {
|
|
12
|
+
queue.push(...current);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const record = current as Record<string, unknown>;
|
|
16
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
17
|
+
if (
|
|
18
|
+
typeof entry === 'string' &&
|
|
19
|
+
(key.toLowerCase().includes('path') || key.toLowerCase().includes('file')) &&
|
|
20
|
+
/\.(png|jpg|jpeg|heic|gif|webp)$/i.test(entry)
|
|
21
|
+
) {
|
|
22
|
+
return entry;
|
|
23
|
+
}
|
|
24
|
+
if (entry && typeof entry === 'object') {
|
|
25
|
+
queue.push(entry);
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(entry)) {
|
|
28
|
+
queue.push(...entry);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function copyPreviewToOutput(sourceImagePath: string, outputArg: string): Promise<string> {
|
|
36
|
+
const candidate = path.resolve(process.cwd(), outputArg);
|
|
37
|
+
let destination = candidate;
|
|
38
|
+
try {
|
|
39
|
+
const stat = await fs.stat(candidate);
|
|
40
|
+
if (stat.isDirectory()) {
|
|
41
|
+
destination = path.join(candidate, path.basename(sourceImagePath));
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
const looksLikeDirectory =
|
|
45
|
+
outputArg.endsWith(path.sep) ||
|
|
46
|
+
outputArg.endsWith('/') ||
|
|
47
|
+
path.extname(path.basename(outputArg)) === '';
|
|
48
|
+
if (looksLikeDirectory) {
|
|
49
|
+
destination = path.join(candidate, path.basename(sourceImagePath));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
await fs.mkdir(path.dirname(destination), { recursive: true });
|
|
53
|
+
await fs.copyFile(sourceImagePath, destination);
|
|
54
|
+
return destination;
|
|
55
|
+
}
|