@myvillage/cli 1.8.4 → 1.9.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/media.js +943 -0
- package/src/index.js +46 -0
- package/src/utils/agentic-templates.js +1 -1
- package/src/utils/api.js +54 -0
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/index.js
CHANGED
|
@@ -57,6 +57,12 @@ import { checkinCommand } from './commands/checkin.js';
|
|
|
57
57
|
import { discoverCommand } from './commands/discover.js';
|
|
58
58
|
import { logCommand } from './commands/log.js';
|
|
59
59
|
import { storyCommand } from './commands/story.js';
|
|
60
|
+
import {
|
|
61
|
+
mediaDraftCreateCommand,
|
|
62
|
+
mediaDraftEditCommand,
|
|
63
|
+
mediaDraftListCommand,
|
|
64
|
+
mediaDraftStatusCommand,
|
|
65
|
+
} from './commands/media.js';
|
|
60
66
|
import {
|
|
61
67
|
soulprintInitCommand,
|
|
62
68
|
soulprintIngestCommand,
|
|
@@ -398,6 +404,46 @@ export function run() {
|
|
|
398
404
|
.option('--contact <name>', 'Contact name')
|
|
399
405
|
.action(bizreqsImportCommand);
|
|
400
406
|
|
|
407
|
+
// ── Media: Draft Submission Pipeline ──────────────────────
|
|
408
|
+
|
|
409
|
+
const mediaCmd = program
|
|
410
|
+
.command('media')
|
|
411
|
+
.description('Media draft submission and approval pipeline');
|
|
412
|
+
|
|
413
|
+
const mediaDraftCmd = mediaCmd
|
|
414
|
+
.command('draft')
|
|
415
|
+
.description('Create and manage social media drafts');
|
|
416
|
+
|
|
417
|
+
mediaDraftCmd
|
|
418
|
+
.command('create')
|
|
419
|
+
.description('Create a new media draft (interactive)')
|
|
420
|
+
.option('--submit', 'Auto-submit for review after creation')
|
|
421
|
+
.option('--file <path...>', 'Attach media files (repeatable)')
|
|
422
|
+
.option('--asset-type <type>', 'Asset type for --file: IMAGE, VIDEO, THUMBNAIL, DOCUMENT (auto-detected if omitted)')
|
|
423
|
+
.action(mediaDraftCreateCommand);
|
|
424
|
+
|
|
425
|
+
mediaDraftCmd
|
|
426
|
+
.command('edit <id>')
|
|
427
|
+
.description('Edit a draft (DRAFT or REJECTED status only)')
|
|
428
|
+
.action(mediaDraftEditCommand);
|
|
429
|
+
|
|
430
|
+
mediaDraftCmd
|
|
431
|
+
.command('list')
|
|
432
|
+
.description('List your media drafts')
|
|
433
|
+
.option('--status <status>', 'Filter: DRAFT, SUBMITTED, IN_REVIEW, APPROVED, REJECTED, SCHEDULED, PUBLISHED')
|
|
434
|
+
.option('--platform <platform>', 'Filter: LINKEDIN, YOUTUBE, TIKTOK, INSTAGRAM')
|
|
435
|
+
.option('--search <query>', 'Search by caption, title, or draft ID')
|
|
436
|
+
.option('--sort <sort>', 'Sort: newest, oldest, publish_date', 'newest')
|
|
437
|
+
.option('-n, --limit <number>', 'Number of results', '20')
|
|
438
|
+
.option('--offset <number>', 'Pagination offset', '0')
|
|
439
|
+
.option('--json', 'Output raw JSON')
|
|
440
|
+
.action(mediaDraftListCommand);
|
|
441
|
+
|
|
442
|
+
mediaDraftCmd
|
|
443
|
+
.command('status <id>')
|
|
444
|
+
.description('Check draft status (e.g., MED-2026-0001)')
|
|
445
|
+
.action(mediaDraftStatusCommand);
|
|
446
|
+
|
|
401
447
|
// ── SoulPrint Studio: Model Training Pipeline ───────────
|
|
402
448
|
|
|
403
449
|
const soulprintCmd = program
|
|
@@ -1398,7 +1398,7 @@ export default function AgentChat() {
|
|
|
1398
1398
|
<div className="agent-chat-messages">
|
|
1399
1399
|
{messages.length === 0 && <p style={{ color: '#999' }}>Ask the agent anything about your village...</p>}
|
|
1400
1400
|
{messages.map((msg, i) => (
|
|
1401
|
-
<div key={i} className={
|
|
1401
|
+
<div key={i} className={\`message \${msg.role}\`}>
|
|
1402
1402
|
<strong>{msg.role === 'user' ? 'You' : 'Agent'}:</strong> {msg.text}
|
|
1403
1403
|
</div>
|
|
1404
1404
|
))}
|
package/src/utils/api.js
CHANGED
|
@@ -466,3 +466,57 @@ export async function getBizReqsSpec(id) {
|
|
|
466
466
|
const response = await client.get(`/${encodeURIComponent(id)}/spec`);
|
|
467
467
|
return response.data;
|
|
468
468
|
}
|
|
469
|
+
|
|
470
|
+
// ── Media Drafts API (/api/media/drafts) ────────────────
|
|
471
|
+
|
|
472
|
+
export async function createMediaDraft(data) {
|
|
473
|
+
const client = getPlatformClient();
|
|
474
|
+
const response = await client.post('/media/drafts', data);
|
|
475
|
+
return response.data;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export async function listMediaDrafts(params = {}) {
|
|
479
|
+
const client = getPlatformClient();
|
|
480
|
+
const response = await client.get('/media/drafts', { params });
|
|
481
|
+
return response.data;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export async function getMediaDraft(id) {
|
|
485
|
+
const client = getPlatformClient();
|
|
486
|
+
const response = await client.get(`/media/drafts/${encodeURIComponent(id)}`);
|
|
487
|
+
return response.data;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export async function updateMediaDraft(id, data) {
|
|
491
|
+
const client = getPlatformClient();
|
|
492
|
+
const response = await client.patch(`/media/drafts/${encodeURIComponent(id)}`, data);
|
|
493
|
+
return response.data;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export async function getMediaDraftStatus(id) {
|
|
497
|
+
const client = getPlatformClient();
|
|
498
|
+
const response = await client.get(`/media/drafts/${encodeURIComponent(id)}/status`);
|
|
499
|
+
return response.data;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export async function uploadMediaDraftAsset(id, formData) {
|
|
503
|
+
const client = getPlatformClient();
|
|
504
|
+
const response = await client.post(`/media/drafts/${encodeURIComponent(id)}/upload`, formData, {
|
|
505
|
+
headers: { 'Content-Type': 'multipart/form-data' },
|
|
506
|
+
maxBodyLength: Infinity,
|
|
507
|
+
maxContentLength: Infinity,
|
|
508
|
+
});
|
|
509
|
+
return response.data;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export async function getMediaDraftUploadUrl(id, data) {
|
|
513
|
+
const client = getPlatformClient();
|
|
514
|
+
const response = await client.post(`/media/drafts/${encodeURIComponent(id)}/upload-url`, data);
|
|
515
|
+
return response.data;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export async function confirmMediaDraftUpload(id, data) {
|
|
519
|
+
const client = getPlatformClient();
|
|
520
|
+
const response = await client.post(`/media/drafts/${encodeURIComponent(id)}/confirm-upload`, data);
|
|
521
|
+
return response.data;
|
|
522
|
+
}
|