@myvillage/cli 1.8.5 → 1.10.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.
- package/package.json +1 -1
- package/src/commands/create-game.js +119 -6
- package/src/commands/deploy.js +143 -18
- package/src/commands/game.js +631 -0
- package/src/commands/media.js +943 -0
- package/src/index.js +72 -1
- package/src/utils/api.js +98 -0
- package/src/utils/templates.js +604 -0
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { readFileSync, statSync, createReadStream } from 'fs';
|
|
4
|
+
import { resolve, basename } from 'path';
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import pLimit from 'p-limit';
|
|
7
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
8
|
+
import { brand, villageSpinner } from '../utils/brand.js';
|
|
9
|
+
import {
|
|
10
|
+
createMediaDraft,
|
|
11
|
+
listMediaDrafts,
|
|
12
|
+
getMediaDraft,
|
|
13
|
+
getMediaDraftStatus,
|
|
14
|
+
updateMediaDraft,
|
|
15
|
+
uploadMediaDraftAsset,
|
|
16
|
+
getMediaDraftUploadUrl,
|
|
17
|
+
confirmMediaDraftUpload,
|
|
18
|
+
} from '../utils/api.js';
|
|
19
|
+
|
|
20
|
+
// ── Status badge colors ────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const STATUS_COLORS = {
|
|
23
|
+
DRAFT: chalk.gray,
|
|
24
|
+
SUBMITTED: chalk.cyan,
|
|
25
|
+
IN_REVIEW: chalk.yellow,
|
|
26
|
+
APPROVED: chalk.green,
|
|
27
|
+
REJECTED: chalk.red,
|
|
28
|
+
SCHEDULED: chalk.magenta,
|
|
29
|
+
PUBLISHED: chalk.greenBright,
|
|
30
|
+
CANCELLED: chalk.gray,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function statusBadge(status) {
|
|
34
|
+
const colorFn = STATUS_COLORS[status] || chalk.white;
|
|
35
|
+
return colorFn(status);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Platform display names ─────────────────────────────
|
|
39
|
+
|
|
40
|
+
const PLATFORM_LABELS = {
|
|
41
|
+
LINKEDIN: 'LinkedIn',
|
|
42
|
+
YOUTUBE: 'YouTube',
|
|
43
|
+
TIKTOK: 'TikTok',
|
|
44
|
+
INSTAGRAM: 'Instagram',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ── Size thresholds ────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
// Files under this size use the simple buffered upload endpoint.
|
|
50
|
+
// Files over this size use presigned URL (direct-to-S3) upload.
|
|
51
|
+
const PRESIGNED_THRESHOLD = 5 * 1024 * 1024; // 5 MB
|
|
52
|
+
|
|
53
|
+
// ── Content type detection ─────────────────────────────
|
|
54
|
+
|
|
55
|
+
const CONTENT_TYPES = {
|
|
56
|
+
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
|
|
57
|
+
gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml',
|
|
58
|
+
mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime',
|
|
59
|
+
avi: 'video/x-msvideo', mkv: 'video/x-matroska',
|
|
60
|
+
pdf: 'application/pdf',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function getContentType(fileName) {
|
|
64
|
+
const ext = fileName.split('.').pop()?.toLowerCase() || '';
|
|
65
|
+
return CONTENT_TYPES[ext] || 'application/octet-stream';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Auto-detect asset type from file extension ─────────
|
|
69
|
+
|
|
70
|
+
function inferAssetType(fileName) {
|
|
71
|
+
const ext = fileName.split('.').pop()?.toLowerCase() || '';
|
|
72
|
+
const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'];
|
|
73
|
+
const videoExts = ['mp4', 'webm', 'mov', 'avi', 'mkv'];
|
|
74
|
+
const docExts = ['pdf'];
|
|
75
|
+
|
|
76
|
+
if (imageExts.includes(ext)) return 'IMAGE';
|
|
77
|
+
if (videoExts.includes(ext)) return 'VIDEO';
|
|
78
|
+
if (docExts.includes(ext)) return 'DOCUMENT';
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Platform-specific metadata prompts ─────────────────
|
|
83
|
+
|
|
84
|
+
async function promptPlatformMetadata(platform, existing = {}) {
|
|
85
|
+
const prompts = [];
|
|
86
|
+
|
|
87
|
+
if (platform === 'YOUTUBE') {
|
|
88
|
+
prompts.push(
|
|
89
|
+
{
|
|
90
|
+
type: 'list',
|
|
91
|
+
name: 'privacy',
|
|
92
|
+
message: 'Privacy:',
|
|
93
|
+
choices: [
|
|
94
|
+
{ name: 'Public', value: 'public' },
|
|
95
|
+
{ name: 'Unlisted', value: 'unlisted' },
|
|
96
|
+
{ name: 'Private', value: 'private' },
|
|
97
|
+
],
|
|
98
|
+
default: existing.privacy || 'public',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
type: 'list',
|
|
102
|
+
name: 'category',
|
|
103
|
+
message: 'Category:',
|
|
104
|
+
choices: [
|
|
105
|
+
{ name: 'Education', value: 'Education' },
|
|
106
|
+
{ name: 'Science & Technology', value: 'Science & Technology' },
|
|
107
|
+
{ name: 'People & Blogs', value: 'People & Blogs' },
|
|
108
|
+
{ name: 'Entertainment', value: 'Entertainment' },
|
|
109
|
+
{ name: 'Howto & Style', value: 'Howto & Style' },
|
|
110
|
+
{ name: 'Gaming', value: 'Gaming' },
|
|
111
|
+
{ name: 'Nonprofits & Activism', value: 'Nonprofits & Activism' },
|
|
112
|
+
],
|
|
113
|
+
default: existing.category || 'Education',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
type: 'input',
|
|
117
|
+
name: 'language',
|
|
118
|
+
message: 'Language (e.g., en, es, fr):',
|
|
119
|
+
default: existing.language || 'en',
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (platform === 'LINKEDIN') {
|
|
125
|
+
prompts.push(
|
|
126
|
+
{
|
|
127
|
+
type: 'list',
|
|
128
|
+
name: 'visibility',
|
|
129
|
+
message: 'Visibility:',
|
|
130
|
+
choices: [
|
|
131
|
+
{ name: 'Public (anyone)', value: 'PUBLIC' },
|
|
132
|
+
{ name: 'Connections only', value: 'CONNECTIONS' },
|
|
133
|
+
],
|
|
134
|
+
default: existing.visibility || 'PUBLIC',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
type: 'list',
|
|
138
|
+
name: 'contentType',
|
|
139
|
+
message: 'Post type:',
|
|
140
|
+
choices: [
|
|
141
|
+
{ name: 'Share / Status update', value: 'share' },
|
|
142
|
+
{ name: 'Article', value: 'article' },
|
|
143
|
+
],
|
|
144
|
+
default: existing.contentType || 'share',
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (platform === 'TIKTOK') {
|
|
150
|
+
prompts.push(
|
|
151
|
+
{
|
|
152
|
+
type: 'list',
|
|
153
|
+
name: 'privacy',
|
|
154
|
+
message: 'Privacy:',
|
|
155
|
+
choices: [
|
|
156
|
+
{ name: 'Public', value: 'PUBLIC_TO_EVERYONE' },
|
|
157
|
+
{ name: 'Friends only', value: 'MUTUAL_FOLLOW_FRIENDS' },
|
|
158
|
+
{ name: 'Private (only me)', value: 'SELF_ONLY' },
|
|
159
|
+
],
|
|
160
|
+
default: existing.privacy || 'PUBLIC_TO_EVERYONE',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
type: 'confirm',
|
|
164
|
+
name: 'allowComments',
|
|
165
|
+
message: 'Allow comments?',
|
|
166
|
+
default: existing.allowComments !== undefined ? existing.allowComments : true,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
type: 'confirm',
|
|
170
|
+
name: 'allowDuet',
|
|
171
|
+
message: 'Allow duets?',
|
|
172
|
+
default: existing.allowDuet !== undefined ? existing.allowDuet : true,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
type: 'confirm',
|
|
176
|
+
name: 'allowStitch',
|
|
177
|
+
message: 'Allow stitches?',
|
|
178
|
+
default: existing.allowStitch !== undefined ? existing.allowStitch : true,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
type: 'confirm',
|
|
182
|
+
name: 'brandedContent',
|
|
183
|
+
message: 'Is this branded content?',
|
|
184
|
+
default: existing.brandedContent || false,
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (platform === 'INSTAGRAM') {
|
|
190
|
+
prompts.push(
|
|
191
|
+
{
|
|
192
|
+
type: 'input',
|
|
193
|
+
name: 'altText',
|
|
194
|
+
message: 'Alt text for accessibility (optional):',
|
|
195
|
+
default: existing.altText || '',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: 'input',
|
|
199
|
+
name: 'location',
|
|
200
|
+
message: 'Location tag (optional):',
|
|
201
|
+
default: existing.location || '',
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
type: 'input',
|
|
205
|
+
name: 'collaborators',
|
|
206
|
+
message: 'Collaborator handles (comma-separated, optional):',
|
|
207
|
+
default: existing.collaborators?.join(', ') || '',
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (prompts.length === 0) return null;
|
|
213
|
+
|
|
214
|
+
console.log(brand.teal(`\n ${PLATFORM_LABELS[platform]} settings:\n`));
|
|
215
|
+
const answers = await inquirer.prompt(prompts);
|
|
216
|
+
|
|
217
|
+
// Clean up empty strings and parse collaborators
|
|
218
|
+
const metadata = {};
|
|
219
|
+
for (const [key, value] of Object.entries(answers)) {
|
|
220
|
+
if (value === '' || value === undefined) continue;
|
|
221
|
+
if (key === 'collaborators' && typeof value === 'string') {
|
|
222
|
+
const parsed = value.split(',').map(s => s.trim()).filter(Boolean);
|
|
223
|
+
if (parsed.length > 0) metadata[key] = parsed;
|
|
224
|
+
} else {
|
|
225
|
+
metadata[key] = value;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return Object.keys(metadata).length > 0 ? metadata : null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── media draft create ─────────────────────────────────
|
|
233
|
+
|
|
234
|
+
export async function mediaDraftCreateCommand(options = {}) {
|
|
235
|
+
if (!isAuthenticated()) {
|
|
236
|
+
console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
console.log(`\n ${brand.gold(chalk.bold('\u2726 Media Draft \u2014 New Submission'))}\n`);
|
|
242
|
+
|
|
243
|
+
// Platform selection
|
|
244
|
+
const platformAnswer = await inquirer.prompt([{
|
|
245
|
+
type: 'list',
|
|
246
|
+
name: 'platform',
|
|
247
|
+
message: 'Target platform:',
|
|
248
|
+
choices: [
|
|
249
|
+
{ name: 'LinkedIn', value: 'LINKEDIN' },
|
|
250
|
+
{ name: 'YouTube', value: 'YOUTUBE' },
|
|
251
|
+
{ name: 'TikTok', value: 'TIKTOK' },
|
|
252
|
+
{ name: 'Instagram', value: 'INSTAGRAM' },
|
|
253
|
+
],
|
|
254
|
+
}]);
|
|
255
|
+
|
|
256
|
+
// Content fields based on platform
|
|
257
|
+
const isVideo = ['YOUTUBE', 'TIKTOK'].includes(platformAnswer.platform);
|
|
258
|
+
|
|
259
|
+
const contentPrompts = [];
|
|
260
|
+
|
|
261
|
+
if (platformAnswer.platform === 'YOUTUBE') {
|
|
262
|
+
contentPrompts.push({
|
|
263
|
+
type: 'input',
|
|
264
|
+
name: 'title',
|
|
265
|
+
message: 'Video title:',
|
|
266
|
+
validate: (v) => v.trim() ? true : 'Title is required for YouTube',
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
contentPrompts.push({
|
|
271
|
+
type: 'input',
|
|
272
|
+
name: 'caption',
|
|
273
|
+
message: isVideo ? 'Description / caption:' : 'Caption:',
|
|
274
|
+
validate: (v, answers) => {
|
|
275
|
+
if (v.trim()) return true;
|
|
276
|
+
if (answers?.title?.trim()) return true;
|
|
277
|
+
return 'Caption is required';
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (isVideo) {
|
|
282
|
+
contentPrompts.push({
|
|
283
|
+
type: 'input',
|
|
284
|
+
name: 'script',
|
|
285
|
+
message: 'Script (optional, press Enter to skip):',
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
contentPrompts.push({
|
|
290
|
+
type: 'input',
|
|
291
|
+
name: 'tags',
|
|
292
|
+
message: 'Tags / hashtags (comma-separated, optional):',
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
contentPrompts.push({
|
|
296
|
+
type: 'input',
|
|
297
|
+
name: 'suggestedPublishAt',
|
|
298
|
+
message: 'Suggested publish date (YYYY-MM-DD or YYYY-MM-DDTHH:mm, optional):',
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const answers = await inquirer.prompt(contentPrompts);
|
|
302
|
+
|
|
303
|
+
// Platform-specific settings
|
|
304
|
+
const platformMetadata = await promptPlatformMetadata(platformAnswer.platform);
|
|
305
|
+
|
|
306
|
+
// Build draft payload
|
|
307
|
+
const data = {
|
|
308
|
+
platform: platformAnswer.platform,
|
|
309
|
+
intakeMethod: 'CLI',
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
if (answers.caption?.trim()) data.caption = answers.caption.trim();
|
|
313
|
+
if (answers.title?.trim()) data.title = answers.title.trim();
|
|
314
|
+
if (answers.script?.trim()) data.script = answers.script.trim();
|
|
315
|
+
if (answers.tags?.trim()) {
|
|
316
|
+
data.tags = answers.tags.split(',').map(t => t.trim()).filter(Boolean);
|
|
317
|
+
}
|
|
318
|
+
if (answers.suggestedPublishAt?.trim()) {
|
|
319
|
+
data.suggestedPublishAt = new Date(answers.suggestedPublishAt.trim()).toISOString();
|
|
320
|
+
}
|
|
321
|
+
if (platformMetadata) {
|
|
322
|
+
data.platformMetadata = platformMetadata;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Auto-submit flag
|
|
326
|
+
if (options.submit) {
|
|
327
|
+
data.submit = true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Create draft
|
|
331
|
+
const spinner = villageSpinner('Creating draft...').start();
|
|
332
|
+
const result = await createMediaDraft(data);
|
|
333
|
+
spinner.succeed('Draft created!');
|
|
334
|
+
|
|
335
|
+
const draft = result.draft;
|
|
336
|
+
console.log(brand.green(`\n \u2713 Draft ID: ${chalk.bold(draft.draftId)}`));
|
|
337
|
+
console.log(brand.teal(` Platform: ${PLATFORM_LABELS[draft.platform]}`));
|
|
338
|
+
console.log(brand.teal(` Status: ${statusBadge(draft.status)}`));
|
|
339
|
+
|
|
340
|
+
// ── File uploads ───────────────────────────────────
|
|
341
|
+
// Collect files from --file flags or interactive prompts
|
|
342
|
+
const filesToUpload = [];
|
|
343
|
+
|
|
344
|
+
if (options.file) {
|
|
345
|
+
// --file can be passed multiple times (commander collects into array)
|
|
346
|
+
const files = Array.isArray(options.file) ? options.file : [options.file];
|
|
347
|
+
for (const f of files) {
|
|
348
|
+
const assetType = options.assetType?.toUpperCase() || inferAssetType(f) || 'IMAGE';
|
|
349
|
+
filesToUpload.push({ path: f, assetType });
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
// Interactive multi-file loop
|
|
353
|
+
let addMore = true;
|
|
354
|
+
while (addMore) {
|
|
355
|
+
const { wantsUpload } = await inquirer.prompt([{
|
|
356
|
+
type: 'confirm',
|
|
357
|
+
name: 'wantsUpload',
|
|
358
|
+
message: filesToUpload.length === 0
|
|
359
|
+
? 'Attach media files?'
|
|
360
|
+
: 'Attach another file?',
|
|
361
|
+
default: false,
|
|
362
|
+
}]);
|
|
363
|
+
|
|
364
|
+
if (!wantsUpload) break;
|
|
365
|
+
|
|
366
|
+
const fileAnswer = await inquirer.prompt([
|
|
367
|
+
{
|
|
368
|
+
type: 'input',
|
|
369
|
+
name: 'filePath',
|
|
370
|
+
message: 'File path:',
|
|
371
|
+
validate: (v) => {
|
|
372
|
+
if (!v.trim()) return 'File path is required';
|
|
373
|
+
try {
|
|
374
|
+
statSync(resolve(v.trim()));
|
|
375
|
+
return true;
|
|
376
|
+
} catch {
|
|
377
|
+
return 'File not found';
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
type: 'list',
|
|
383
|
+
name: 'assetType',
|
|
384
|
+
message: 'Asset type:',
|
|
385
|
+
choices: [
|
|
386
|
+
{ name: 'Image', value: 'IMAGE' },
|
|
387
|
+
{ name: 'Video', value: 'VIDEO' },
|
|
388
|
+
{ name: 'Thumbnail', value: 'THUMBNAIL' },
|
|
389
|
+
{ name: 'Document', value: 'DOCUMENT' },
|
|
390
|
+
],
|
|
391
|
+
default: () => {
|
|
392
|
+
// Try to auto-detect from the last input
|
|
393
|
+
return undefined;
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
]);
|
|
397
|
+
|
|
398
|
+
filesToUpload.push({ path: fileAnswer.filePath.trim(), assetType: fileAnswer.assetType });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Upload all collected files
|
|
403
|
+
if (filesToUpload.length > 0) {
|
|
404
|
+
console.log(brand.teal(`\n Uploading ${filesToUpload.length} file${filesToUpload.length > 1 ? 's' : ''}...\n`));
|
|
405
|
+
await uploadFiles(draft.id, filesToUpload);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Submit prompt if not already submitted
|
|
409
|
+
if (!options.submit && draft.status === 'DRAFT') {
|
|
410
|
+
const { wantsSubmit } = await inquirer.prompt([{
|
|
411
|
+
type: 'confirm',
|
|
412
|
+
name: 'wantsSubmit',
|
|
413
|
+
message: 'Submit for review now?',
|
|
414
|
+
default: true,
|
|
415
|
+
}]);
|
|
416
|
+
|
|
417
|
+
if (wantsSubmit) {
|
|
418
|
+
const submitSpinner = villageSpinner('Submitting for review...').start();
|
|
419
|
+
try {
|
|
420
|
+
await updateMediaDraft(draft.id, { submit: true });
|
|
421
|
+
submitSpinner.succeed('Submitted for review!');
|
|
422
|
+
} catch (err) {
|
|
423
|
+
const message = err.response?.data?.error || err.message;
|
|
424
|
+
submitSpinner.fail(`Failed to submit: ${message}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
console.log(brand.teal(`\n Track status: myvillage media draft status ${draft.draftId}\n`));
|
|
430
|
+
} catch (err) {
|
|
431
|
+
const message = err.response?.data?.error || err.response?.data?.message || err.message;
|
|
432
|
+
console.log(chalk.red(` \u2717 Failed to create draft: ${message}\n`));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ── Multi-file upload orchestrator ─────────────────────
|
|
437
|
+
|
|
438
|
+
async function uploadFiles(draftId, files) {
|
|
439
|
+
const limit = pLimit(3); // Max 3 concurrent uploads
|
|
440
|
+
|
|
441
|
+
const tasks = files.map((file) =>
|
|
442
|
+
limit(() => uploadSingleFile(draftId, file.path, file.assetType))
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
await Promise.all(tasks);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ── Single file upload (auto-selects strategy) ─────────
|
|
449
|
+
|
|
450
|
+
async function uploadSingleFile(draftId, filePath, assetType) {
|
|
451
|
+
const resolvedPath = resolve(filePath);
|
|
452
|
+
const fileName = basename(resolvedPath);
|
|
453
|
+
|
|
454
|
+
let fileSize;
|
|
455
|
+
try {
|
|
456
|
+
const stat = statSync(resolvedPath);
|
|
457
|
+
fileSize = stat.size;
|
|
458
|
+
} catch (err) {
|
|
459
|
+
console.log(chalk.red(` \u2717 Could not read file: ${err.message}`));
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Use presigned upload for large files, direct upload for small ones
|
|
464
|
+
if (fileSize > PRESIGNED_THRESHOLD) {
|
|
465
|
+
await uploadViaPresignedUrl(draftId, resolvedPath, fileName, fileSize, assetType);
|
|
466
|
+
} else {
|
|
467
|
+
await uploadViaDirect(draftId, resolvedPath, fileName, assetType);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ── Presigned URL upload (large files — direct to S3) ──
|
|
472
|
+
|
|
473
|
+
async function uploadViaPresignedUrl(draftId, resolvedPath, fileName, fileSize, assetType) {
|
|
474
|
+
const contentType = getContentType(fileName);
|
|
475
|
+
const spinner = villageSpinner(`${fileName} (${formatBytes(fileSize)}) — requesting upload URL...`).start();
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
// 1. Get presigned URL from API
|
|
479
|
+
const urlResult = await getMediaDraftUploadUrl(draftId, {
|
|
480
|
+
fileName,
|
|
481
|
+
assetType,
|
|
482
|
+
contentType,
|
|
483
|
+
fileSize,
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// 2. Upload directly to S3 with progress
|
|
487
|
+
spinner.text = chalk.yellow(`${fileName} — uploading to storage...`);
|
|
488
|
+
|
|
489
|
+
const fileBuffer = readFileSync(resolvedPath);
|
|
490
|
+
|
|
491
|
+
await axios.put(urlResult.uploadUrl, fileBuffer, {
|
|
492
|
+
headers: {
|
|
493
|
+
'Content-Type': contentType,
|
|
494
|
+
},
|
|
495
|
+
maxBodyLength: Infinity,
|
|
496
|
+
maxContentLength: Infinity,
|
|
497
|
+
onUploadProgress: (progressEvent) => {
|
|
498
|
+
if (progressEvent.total) {
|
|
499
|
+
const pct = Math.round((progressEvent.loaded / progressEvent.total) * 100);
|
|
500
|
+
spinner.text = chalk.yellow(`${fileName} — ${pct}% (${formatBytes(progressEvent.loaded)} / ${formatBytes(progressEvent.total)})`);
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// 3. Confirm upload with API
|
|
506
|
+
spinner.text = chalk.yellow(`${fileName} — confirming...`);
|
|
507
|
+
|
|
508
|
+
const confirmResult = await confirmMediaDraftUpload(draftId, {
|
|
509
|
+
s3Key: urlResult.s3Key,
|
|
510
|
+
fileName,
|
|
511
|
+
assetType,
|
|
512
|
+
contentType,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
spinner.succeed(`${fileName} (${formatBytes(fileSize)})`);
|
|
516
|
+
console.log(brand.teal(` Type: ${confirmResult.asset.assetType} Uploaded via presigned URL`));
|
|
517
|
+
} catch (err) {
|
|
518
|
+
const message = err.response?.data?.error || err.message;
|
|
519
|
+
spinner.fail(`${fileName} — upload failed: ${message}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ── Direct upload (small files — via API server) ───────
|
|
524
|
+
|
|
525
|
+
async function uploadViaDirect(draftId, resolvedPath, fileName, assetType) {
|
|
526
|
+
const spinner = villageSpinner(`${fileName} — uploading...`).start();
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
const fileBuffer = readFileSync(resolvedPath);
|
|
530
|
+
const formData = new FormData();
|
|
531
|
+
const blob = new Blob([fileBuffer]);
|
|
532
|
+
formData.append('file', blob, fileName);
|
|
533
|
+
formData.append('assetType', assetType || 'IMAGE');
|
|
534
|
+
|
|
535
|
+
const result = await uploadMediaDraftAsset(draftId, formData);
|
|
536
|
+
spinner.succeed(`${fileName} (${formatBytes(result.asset.fileSize)})`);
|
|
537
|
+
console.log(brand.teal(` Type: ${result.asset.assetType}`));
|
|
538
|
+
} catch (err) {
|
|
539
|
+
const message = err.response?.data?.error || err.message;
|
|
540
|
+
spinner.fail(`${fileName} — upload failed: ${message}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ── Bytes formatter ────────────────────────────────────
|
|
545
|
+
|
|
546
|
+
function formatBytes(bytes) {
|
|
547
|
+
if (!bytes) return 'unknown';
|
|
548
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
549
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
550
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
551
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ── media draft edit ───────────────────────────────────
|
|
555
|
+
|
|
556
|
+
export async function mediaDraftEditCommand(id) {
|
|
557
|
+
if (!isAuthenticated()) {
|
|
558
|
+
console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (!id) {
|
|
563
|
+
console.log(chalk.red(' \u2717 Draft ID is required (e.g., MED-2026-0001)'));
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const spinner = villageSpinner(`Loading draft ${id}...`).start();
|
|
568
|
+
|
|
569
|
+
let draft;
|
|
570
|
+
try {
|
|
571
|
+
const result = await getMediaDraft(id);
|
|
572
|
+
draft = result.draft;
|
|
573
|
+
spinner.stop();
|
|
574
|
+
} catch (err) {
|
|
575
|
+
if (err.response?.status === 404) {
|
|
576
|
+
spinner.fail(`Draft not found: ${id}`);
|
|
577
|
+
} else {
|
|
578
|
+
spinner.fail(`Failed to load draft: ${err.response?.data?.error || err.message}`);
|
|
579
|
+
}
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (!['DRAFT', 'REJECTED'].includes(draft.status)) {
|
|
584
|
+
console.log(chalk.red(`\n \u2717 Cannot edit draft in ${draft.status} status.`));
|
|
585
|
+
console.log(brand.teal(' Only DRAFT or REJECTED drafts can be edited.\n'));
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const platform = draft.platform;
|
|
590
|
+
const isVideo = ['YOUTUBE', 'TIKTOK'].includes(platform);
|
|
591
|
+
|
|
592
|
+
console.log(`\n ${brand.gold(chalk.bold(`\u2726 Edit Draft \u2014 ${draft.draftId}`))}`);
|
|
593
|
+
console.log(brand.teal(` Platform: ${PLATFORM_LABELS[platform]} Status: ${statusBadge(draft.status)}`));
|
|
594
|
+
|
|
595
|
+
if (draft.rejectionReason) {
|
|
596
|
+
console.log(chalk.red(` Rejection reason: ${draft.rejectionReason}`));
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (draft.reviews?.length > 0) {
|
|
600
|
+
const lastReview = draft.reviews[0]; // already sorted desc
|
|
601
|
+
if (lastReview.notes) {
|
|
602
|
+
console.log(brand.cream(` Last review notes: ${lastReview.notes}`));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
console.log(brand.cream('\n Press Enter to keep current value, or type a new one.\n'));
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
// Content fields
|
|
610
|
+
const contentPrompts = [];
|
|
611
|
+
|
|
612
|
+
if (platform === 'YOUTUBE') {
|
|
613
|
+
contentPrompts.push({
|
|
614
|
+
type: 'input',
|
|
615
|
+
name: 'title',
|
|
616
|
+
message: `Video title:`,
|
|
617
|
+
default: draft.title || '',
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
contentPrompts.push({
|
|
622
|
+
type: 'input',
|
|
623
|
+
name: 'caption',
|
|
624
|
+
message: isVideo ? 'Description / caption:' : 'Caption:',
|
|
625
|
+
default: draft.caption || '',
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
if (isVideo) {
|
|
629
|
+
contentPrompts.push({
|
|
630
|
+
type: 'input',
|
|
631
|
+
name: 'script',
|
|
632
|
+
message: 'Script:',
|
|
633
|
+
default: draft.script || '',
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
contentPrompts.push({
|
|
638
|
+
type: 'input',
|
|
639
|
+
name: 'tags',
|
|
640
|
+
message: 'Tags / hashtags (comma-separated):',
|
|
641
|
+
default: draft.tags?.join(', ') || '',
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
contentPrompts.push({
|
|
645
|
+
type: 'input',
|
|
646
|
+
name: 'suggestedPublishAt',
|
|
647
|
+
message: 'Suggested publish date (YYYY-MM-DD or YYYY-MM-DDTHH:mm):',
|
|
648
|
+
default: draft.suggestedPublishAt
|
|
649
|
+
? new Date(draft.suggestedPublishAt).toISOString().slice(0, 16)
|
|
650
|
+
: '',
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const answers = await inquirer.prompt(contentPrompts);
|
|
654
|
+
|
|
655
|
+
// Platform-specific settings
|
|
656
|
+
const existingMetadata = draft.platformMetadata || {};
|
|
657
|
+
const platformMetadata = await promptPlatformMetadata(platform, existingMetadata);
|
|
658
|
+
|
|
659
|
+
// Build update payload — only send changed fields
|
|
660
|
+
const updates = {};
|
|
661
|
+
|
|
662
|
+
if (platform === 'YOUTUBE' && answers.title !== (draft.title || '')) {
|
|
663
|
+
updates.title = answers.title.trim() || undefined;
|
|
664
|
+
}
|
|
665
|
+
if (answers.caption !== (draft.caption || '')) {
|
|
666
|
+
updates.caption = answers.caption.trim() || undefined;
|
|
667
|
+
}
|
|
668
|
+
if (isVideo && answers.script !== (draft.script || '')) {
|
|
669
|
+
updates.script = answers.script.trim() || undefined;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const newTags = answers.tags
|
|
673
|
+
? answers.tags.split(',').map(t => t.trim()).filter(Boolean)
|
|
674
|
+
: [];
|
|
675
|
+
const oldTags = draft.tags || [];
|
|
676
|
+
if (JSON.stringify(newTags) !== JSON.stringify(oldTags)) {
|
|
677
|
+
updates.tags = newTags;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const oldPublish = draft.suggestedPublishAt
|
|
681
|
+
? new Date(draft.suggestedPublishAt).toISOString().slice(0, 16)
|
|
682
|
+
: '';
|
|
683
|
+
if (answers.suggestedPublishAt !== oldPublish) {
|
|
684
|
+
updates.suggestedPublishAt = answers.suggestedPublishAt.trim()
|
|
685
|
+
? new Date(answers.suggestedPublishAt.trim()).toISOString()
|
|
686
|
+
: null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (platformMetadata) {
|
|
690
|
+
updates.platformMetadata = platformMetadata;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// File uploads
|
|
694
|
+
const filesToUpload = [];
|
|
695
|
+
let addMore = true;
|
|
696
|
+
const existingAssets = draft.assets || [];
|
|
697
|
+
|
|
698
|
+
if (existingAssets.length > 0) {
|
|
699
|
+
console.log(brand.teal(`\n Current files (${existingAssets.length}):`));
|
|
700
|
+
for (const asset of existingAssets) {
|
|
701
|
+
console.log(` ${chalk.dim(asset.assetType)} ${asset.fileName} (${formatBytes(asset.fileSize)})`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
while (addMore) {
|
|
706
|
+
const { wantsUpload } = await inquirer.prompt([{
|
|
707
|
+
type: 'confirm',
|
|
708
|
+
name: 'wantsUpload',
|
|
709
|
+
message: filesToUpload.length === 0
|
|
710
|
+
? 'Attach additional media files?'
|
|
711
|
+
: 'Attach another file?',
|
|
712
|
+
default: false,
|
|
713
|
+
}]);
|
|
714
|
+
|
|
715
|
+
if (!wantsUpload) break;
|
|
716
|
+
|
|
717
|
+
const fileAnswer = await inquirer.prompt([
|
|
718
|
+
{
|
|
719
|
+
type: 'input',
|
|
720
|
+
name: 'filePath',
|
|
721
|
+
message: 'File path:',
|
|
722
|
+
validate: (v) => {
|
|
723
|
+
if (!v.trim()) return 'File path is required';
|
|
724
|
+
try {
|
|
725
|
+
statSync(resolve(v.trim()));
|
|
726
|
+
return true;
|
|
727
|
+
} catch {
|
|
728
|
+
return 'File not found';
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
type: 'list',
|
|
734
|
+
name: 'assetType',
|
|
735
|
+
message: 'Asset type:',
|
|
736
|
+
choices: [
|
|
737
|
+
{ name: 'Image', value: 'IMAGE' },
|
|
738
|
+
{ name: 'Video', value: 'VIDEO' },
|
|
739
|
+
{ name: 'Thumbnail', value: 'THUMBNAIL' },
|
|
740
|
+
{ name: 'Document', value: 'DOCUMENT' },
|
|
741
|
+
],
|
|
742
|
+
},
|
|
743
|
+
]);
|
|
744
|
+
|
|
745
|
+
filesToUpload.push({ path: fileAnswer.filePath.trim(), assetType: fileAnswer.assetType });
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Apply updates
|
|
749
|
+
const hasFieldChanges = Object.keys(updates).length > 0;
|
|
750
|
+
const hasFileChanges = filesToUpload.length > 0;
|
|
751
|
+
|
|
752
|
+
if (!hasFieldChanges && !hasFileChanges) {
|
|
753
|
+
console.log(brand.teal('\n No changes made.\n'));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (hasFieldChanges) {
|
|
758
|
+
const updateSpinner = villageSpinner('Updating draft...').start();
|
|
759
|
+
try {
|
|
760
|
+
await updateMediaDraft(draft.id, updates);
|
|
761
|
+
updateSpinner.succeed('Draft updated!');
|
|
762
|
+
} catch (err) {
|
|
763
|
+
const message = err.response?.data?.error || err.message;
|
|
764
|
+
updateSpinner.fail(`Failed to update: ${message}`);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (hasFileChanges) {
|
|
770
|
+
console.log(brand.teal(`\n Uploading ${filesToUpload.length} file${filesToUpload.length > 1 ? 's' : ''}...\n`));
|
|
771
|
+
await uploadFiles(draft.id, filesToUpload);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Offer to resubmit if it was rejected
|
|
775
|
+
if (draft.status === 'REJECTED' || draft.status === 'DRAFT') {
|
|
776
|
+
const { wantsSubmit } = await inquirer.prompt([{
|
|
777
|
+
type: 'confirm',
|
|
778
|
+
name: 'wantsSubmit',
|
|
779
|
+
message: draft.status === 'REJECTED' ? 'Resubmit for review?' : 'Submit for review?',
|
|
780
|
+
default: true,
|
|
781
|
+
}]);
|
|
782
|
+
|
|
783
|
+
if (wantsSubmit) {
|
|
784
|
+
const submitSpinner = villageSpinner('Submitting for review...').start();
|
|
785
|
+
try {
|
|
786
|
+
await updateMediaDraft(draft.id, { submit: true });
|
|
787
|
+
submitSpinner.succeed(draft.status === 'REJECTED' ? 'Resubmitted for review!' : 'Submitted for review!');
|
|
788
|
+
} catch (err) {
|
|
789
|
+
const message = err.response?.data?.error || err.message;
|
|
790
|
+
submitSpinner.fail(`Failed to submit: ${message}`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
console.log(brand.teal(`\n Track status: myvillage media draft status ${draft.draftId}\n`));
|
|
796
|
+
} catch (err) {
|
|
797
|
+
const message = err.response?.data?.error || err.response?.data?.message || err.message;
|
|
798
|
+
console.log(chalk.red(` \u2717 Failed to edit draft: ${message}\n`));
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ── media draft list ───────────────────────────────────
|
|
803
|
+
|
|
804
|
+
export async function mediaDraftListCommand(options = {}) {
|
|
805
|
+
if (!isAuthenticated()) {
|
|
806
|
+
console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const spinner = villageSpinner('Loading drafts...').start();
|
|
811
|
+
|
|
812
|
+
try {
|
|
813
|
+
const params = {
|
|
814
|
+
limit: parseInt(options.limit) || 20,
|
|
815
|
+
offset: parseInt(options.offset) || 0,
|
|
816
|
+
sort: options.sort || 'newest',
|
|
817
|
+
};
|
|
818
|
+
if (options.status) params.status = options.status.toUpperCase();
|
|
819
|
+
if (options.platform) params.platform = options.platform.toUpperCase();
|
|
820
|
+
if (options.search) params.search = options.search;
|
|
821
|
+
|
|
822
|
+
const result = await listMediaDrafts(params);
|
|
823
|
+
spinner.stop();
|
|
824
|
+
|
|
825
|
+
if (options.json) {
|
|
826
|
+
console.log(JSON.stringify(result, null, 2));
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const drafts = result.drafts || [];
|
|
831
|
+
|
|
832
|
+
if (drafts.length === 0) {
|
|
833
|
+
console.log(brand.teal('\n No media drafts found.\n'));
|
|
834
|
+
console.log(brand.cream(' Create one with: myvillage media draft create\n'));
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const statusLabel = options.status
|
|
839
|
+
? ` \u2014 ${options.status.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}`
|
|
840
|
+
: '';
|
|
841
|
+
|
|
842
|
+
console.log(`\n ${chalk.bold(`Media Drafts${statusLabel}`)}\n`);
|
|
843
|
+
|
|
844
|
+
for (const draft of drafts) {
|
|
845
|
+
const platform = PLATFORM_LABELS[draft.platform] || draft.platform;
|
|
846
|
+
const status = statusBadge(draft.status);
|
|
847
|
+
const preview = draft.caption
|
|
848
|
+
? draft.caption.substring(0, 60) + (draft.caption.length > 60 ? '...' : '')
|
|
849
|
+
: draft.title || draft.script?.substring(0, 60) || '(no content)';
|
|
850
|
+
const date = new Date(draft.createdAt).toLocaleDateString();
|
|
851
|
+
const assetCount = draft.assets?.length || draft._count?.assets || 0;
|
|
852
|
+
const assetLabel = assetCount > 0 ? chalk.dim(` [${assetCount} file${assetCount > 1 ? 's' : ''}]`) : '';
|
|
853
|
+
|
|
854
|
+
console.log(` ${brand.gold(draft.draftId)} ${status} ${chalk.dim(platform)} ${chalk.dim(date)}${assetLabel}`);
|
|
855
|
+
console.log(` ${brand.cream(preview)}`);
|
|
856
|
+
console.log('');
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const { pagination } = result;
|
|
860
|
+
if (pagination) {
|
|
861
|
+
console.log(brand.teal(` Showing ${drafts.length} of ${pagination.total} drafts`));
|
|
862
|
+
if (pagination.hasMore) {
|
|
863
|
+
console.log(brand.teal(` More results: --offset=${pagination.offset + pagination.limit}`));
|
|
864
|
+
}
|
|
865
|
+
console.log('');
|
|
866
|
+
}
|
|
867
|
+
} catch (err) {
|
|
868
|
+
const message = err.response?.data?.error || err.message;
|
|
869
|
+
spinner.fail(`Failed to load drafts: ${message}`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// ── media draft status ─────────────────────────────────
|
|
874
|
+
|
|
875
|
+
export async function mediaDraftStatusCommand(id) {
|
|
876
|
+
if (!isAuthenticated()) {
|
|
877
|
+
console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (!id) {
|
|
882
|
+
console.log(chalk.red(' \u2717 Draft ID is required (e.g., MED-2026-0001)'));
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const spinner = villageSpinner(`Loading status for ${id}...`).start();
|
|
887
|
+
|
|
888
|
+
try {
|
|
889
|
+
const result = await getMediaDraftStatus(id);
|
|
890
|
+
spinner.stop();
|
|
891
|
+
|
|
892
|
+
const platform = PLATFORM_LABELS[result.platform] || result.platform;
|
|
893
|
+
const status = statusBadge(result.status);
|
|
894
|
+
|
|
895
|
+
console.log(`\n ${chalk.bold('Media Draft Status')}\n`);
|
|
896
|
+
console.log(` ${brand.gold('Draft ID:')} ${result.draftId}`);
|
|
897
|
+
console.log(` ${brand.gold('Status:')} ${status}`);
|
|
898
|
+
console.log(` ${brand.gold('Platform:')} ${platform}`);
|
|
899
|
+
|
|
900
|
+
if (result.title) {
|
|
901
|
+
console.log(` ${brand.gold('Title:')} ${result.title}`);
|
|
902
|
+
}
|
|
903
|
+
if (result.caption) {
|
|
904
|
+
console.log(` ${brand.gold('Caption:')} ${result.caption}`);
|
|
905
|
+
}
|
|
906
|
+
if (result.suggestedPublishAt) {
|
|
907
|
+
console.log(` ${brand.gold('Publish at:')} ${new Date(result.suggestedPublishAt).toLocaleString()}`);
|
|
908
|
+
}
|
|
909
|
+
if (result.publishedAt) {
|
|
910
|
+
console.log(` ${brand.gold('Published:')} ${new Date(result.publishedAt).toLocaleString()}`);
|
|
911
|
+
}
|
|
912
|
+
if (result.platformPostUrl) {
|
|
913
|
+
console.log(` ${brand.gold('Post URL:')} ${result.platformPostUrl}`);
|
|
914
|
+
}
|
|
915
|
+
if (result.rejectionReason) {
|
|
916
|
+
console.log(` ${chalk.red('Rejection:')} ${result.rejectionReason}`);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
console.log(` ${brand.gold('Created:')} ${new Date(result.createdAt).toLocaleString()}`);
|
|
920
|
+
console.log(` ${brand.gold('Updated:')} ${new Date(result.updatedAt).toLocaleString()}`);
|
|
921
|
+
|
|
922
|
+
if (result.reviewCount > 0) {
|
|
923
|
+
console.log(` ${brand.gold('Reviews:')} ${result.reviewCount}`);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (result.lastReview) {
|
|
927
|
+
const review = result.lastReview;
|
|
928
|
+
console.log(`\n ${chalk.bold('Latest Review')}`);
|
|
929
|
+
console.log(` Action: ${review.action}`);
|
|
930
|
+
if (review.notes) console.log(` Notes: ${review.notes}`);
|
|
931
|
+
console.log(` Date: ${new Date(review.createdAt).toLocaleString()}`);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
console.log('');
|
|
935
|
+
} catch (err) {
|
|
936
|
+
if (err.response?.status === 404) {
|
|
937
|
+
spinner.fail(`Draft not found: ${id}`);
|
|
938
|
+
} else {
|
|
939
|
+
const message = err.response?.data?.error || err.message;
|
|
940
|
+
spinner.fail(`Failed to load status: ${message}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|