@socialized.dev/cli 0.1.1 → 0.3.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/README.md +8 -2
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +338 -204
- package/package.json +5 -13
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
npm install -g @socialized.dev/cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
This installs the `socialized.dev` command:
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
|
|
14
|
+
socialized.dev --help
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## Auth
|
|
@@ -43,6 +43,12 @@ socialized.dev publications create \
|
|
|
43
43
|
`publications create` uploads local video and cover files internally, then creates the
|
|
44
44
|
publication. You can also pass an existing `asset_...` id to `--video` or `--cover`.
|
|
45
45
|
|
|
46
|
+
YouTube targets take `--youtube-tags`, `--youtube-category`, `--youtube-language`,
|
|
47
|
+
`--youtube-audio-language`, `--youtube-privacy`, `--youtube-made-for-kids`,
|
|
48
|
+
`--youtube-synthetic-media`, `--youtube-publish-at` (schedule the release), `--youtube-caption`
|
|
49
|
+
and `--youtube-playlist`; run
|
|
50
|
+
`socialized.dev publications create --help` for the full list.
|
|
51
|
+
|
|
46
52
|
## Brands
|
|
47
53
|
|
|
48
54
|
If your account has multiple brands, you can switch between them:
|
package/dist/bin.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {}
|
package/dist/bin.js
CHANGED
|
@@ -6,7 +6,7 @@ import { basename, extname, join } from "node:path";
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { Readable } from "node:stream";
|
|
8
8
|
//#region package.json
|
|
9
|
-
var version = "0.
|
|
9
|
+
var version = "0.3.0";
|
|
10
10
|
//#endregion
|
|
11
11
|
//#region src/api.ts
|
|
12
12
|
var ApiError = class extends Error {
|
|
@@ -45,7 +45,7 @@ const createApiClient = (config) => {
|
|
|
45
45
|
if (!res.ok) throw new ApiError(res.status, await res.text(), init.method ?? "GET", path);
|
|
46
46
|
return res;
|
|
47
47
|
};
|
|
48
|
-
const json = (path, init) => request(path, init)
|
|
48
|
+
const json = async (path, init) => (await request(path, init)).json();
|
|
49
49
|
return {
|
|
50
50
|
listBrands: () => json("/brands"),
|
|
51
51
|
startAssetUpload: (body) => json("/assets", {
|
|
@@ -139,7 +139,8 @@ const loginCommand = buildCommand({
|
|
|
139
139
|
const piped = flags.key === void 0 && !process.stdin.isTTY ? (await readStdin()).trim() : void 0;
|
|
140
140
|
const key = (flags.key ?? piped)?.trim();
|
|
141
141
|
if (!key) throw new UsageError("Pass an API key with `--key sk_...` or pipe one to stdin.");
|
|
142
|
-
const
|
|
142
|
+
const config = resolveConfig({ apiKey: key });
|
|
143
|
+
const { brands } = await createApiClient(config).listBrands();
|
|
143
144
|
saveConfig({
|
|
144
145
|
apiKey: key,
|
|
145
146
|
brandId: brands.length === 1 ? brands[0]?.id : void 0
|
|
@@ -187,43 +188,45 @@ const resolveBrand = (brands, selector) => {
|
|
|
187
188
|
if (matches.length > 1) throw new UsageError(`Brand name is ambiguous: ${selector}`);
|
|
188
189
|
throw new UsageError(`Brand not found: ${selector}`);
|
|
189
190
|
};
|
|
191
|
+
const listCommand = buildCommand({
|
|
192
|
+
docs: { brief: "List brands available to the API key" },
|
|
193
|
+
parameters: { flags: { json: {
|
|
194
|
+
kind: "boolean",
|
|
195
|
+
brief: "Print JSON output"
|
|
196
|
+
} } },
|
|
197
|
+
async func(flags) {
|
|
198
|
+
requireAuth(this);
|
|
199
|
+
const { brands } = await this.api.listBrands();
|
|
200
|
+
if (flags.json) return jsonOut(this, { brands });
|
|
201
|
+
if (brands.length === 0) {
|
|
202
|
+
out(this, "No brands found. Go to the web dashboard to create one.");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
for (const brand of brands) out(this, `${brand.id}\t${brand.name}\t${brand.connectedPlatforms.join(",") || "-"}`);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
const useCommand = buildCommand({
|
|
209
|
+
docs: { brief: "Persist the default brand for publication commands" },
|
|
210
|
+
parameters: { positional: {
|
|
211
|
+
kind: "tuple",
|
|
212
|
+
parameters: [{
|
|
213
|
+
brief: "Brand id or exact brand name",
|
|
214
|
+
parse: String
|
|
215
|
+
}]
|
|
216
|
+
} },
|
|
217
|
+
async func(_flags, selector) {
|
|
218
|
+
requireAuth(this);
|
|
219
|
+
const { brands } = await this.api.listBrands();
|
|
220
|
+
if (brands.length === 0) throw new UsageError("No brands found. Go to the web dashboard to create one.");
|
|
221
|
+
const brand = resolveBrand(brands, selector);
|
|
222
|
+
saveConfig({ brandId: brand.id });
|
|
223
|
+
out(this, `Using ${brand.name} (${brand.id}).`);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
190
226
|
const brandsRoutes = buildRouteMap({
|
|
191
227
|
routes: {
|
|
192
|
-
list:
|
|
193
|
-
|
|
194
|
-
parameters: { flags: { json: {
|
|
195
|
-
kind: "boolean",
|
|
196
|
-
brief: "Print JSON output"
|
|
197
|
-
} } },
|
|
198
|
-
async func(flags) {
|
|
199
|
-
requireAuth(this);
|
|
200
|
-
const { brands } = await this.api.listBrands();
|
|
201
|
-
if (flags.json) return jsonOut(this, { brands });
|
|
202
|
-
if (brands.length === 0) {
|
|
203
|
-
out(this, "No brands found. Go to the web dashboard to create one.");
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
for (const brand of brands) out(this, `${brand.id}\t${brand.name}\t${brand.connectedPlatforms.join(",") || "-"}`);
|
|
207
|
-
}
|
|
208
|
-
}),
|
|
209
|
-
use: buildCommand({
|
|
210
|
-
docs: { brief: "Persist the default brand for publication commands" },
|
|
211
|
-
parameters: { positional: {
|
|
212
|
-
kind: "tuple",
|
|
213
|
-
parameters: [{
|
|
214
|
-
brief: "Brand id or exact brand name",
|
|
215
|
-
parse: String
|
|
216
|
-
}]
|
|
217
|
-
} },
|
|
218
|
-
async func(_flags, selector) {
|
|
219
|
-
requireAuth(this);
|
|
220
|
-
const { brands } = await this.api.listBrands();
|
|
221
|
-
if (brands.length === 0) throw new UsageError("No brands found. Go to the web dashboard to create one.");
|
|
222
|
-
const brand = resolveBrand(brands, selector);
|
|
223
|
-
saveConfig({ brandId: brand.id });
|
|
224
|
-
out(this, `Using ${brand.name} (${brand.id}).`);
|
|
225
|
-
}
|
|
226
|
-
})
|
|
228
|
+
list: listCommand,
|
|
229
|
+
use: useCommand
|
|
227
230
|
},
|
|
228
231
|
docs: { brief: "Manage default brand selection" }
|
|
229
232
|
});
|
|
@@ -240,6 +243,48 @@ const parseTargets = (value) => {
|
|
|
240
243
|
}
|
|
241
244
|
return targets;
|
|
242
245
|
};
|
|
246
|
+
const parseTags = (value) => value.split(",").map((tag) => tag.trim()).filter(Boolean);
|
|
247
|
+
const ISO_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})?$/;
|
|
248
|
+
/** A date-time the user typed (an offset-less one reads as local time) → the UTC ISO string the API
|
|
249
|
+
* takes. Strict: `Date` alone would silently roll 2026-02-30 over to March 2 — the wrong release day. */
|
|
250
|
+
const parseDateTime = (value) => {
|
|
251
|
+
const fields = ISO_DATE_TIME.exec(value)?.slice(1, 7).map((field) => Number(field ?? "0"));
|
|
252
|
+
const [year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0] = fields ?? [];
|
|
253
|
+
const calendar = new Date(Date.UTC(year, month - 1, day, hour, minute, second));
|
|
254
|
+
const date = new Date(value);
|
|
255
|
+
if (fields === void 0 || calendar.getUTCFullYear() !== year || calendar.getUTCMonth() !== month - 1 || calendar.getUTCDate() !== day || calendar.getUTCHours() !== hour || calendar.getUTCMinutes() !== minute || calendar.getUTCSeconds() !== second || Number.isNaN(date.getTime())) throw new UsageError(`Not a date-time: ${value} (e.g. 2026-10-01T09:00 or 2026-10-01T09:00Z)`);
|
|
256
|
+
return date.toISOString();
|
|
257
|
+
};
|
|
258
|
+
/** `--youtube-caption <lang>[:<name>]=<path|asset_id>` — e.g. `en=./subs.srt`, `en:SDH=./sdh.vtt`.
|
|
259
|
+
* Language tags never contain `:` or `=`, so the first of each splits unambiguously. */
|
|
260
|
+
const parseCaption = (value) => {
|
|
261
|
+
const eq = value.indexOf("=");
|
|
262
|
+
const track = eq === -1 ? "" : value.slice(0, eq);
|
|
263
|
+
const source = eq === -1 ? "" : value.slice(eq + 1);
|
|
264
|
+
const colon = track.indexOf(":");
|
|
265
|
+
const language = colon === -1 ? track : track.slice(0, colon);
|
|
266
|
+
const name = colon === -1 ? void 0 : track.slice(colon + 1);
|
|
267
|
+
if (language === "" || source === "") throw new UsageError(`Not a caption: ${value} (e.g. en=./subs.srt or en:SDH=./sdh.vtt)`);
|
|
268
|
+
return {
|
|
269
|
+
language,
|
|
270
|
+
source,
|
|
271
|
+
...name ? { name } : {}
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
/** The YouTube target's `options` bag from the `--youtube-*` flags (captions join later, once their
|
|
275
|
+
* files are uploaded). Only set fields are sent, so the API's defaults (public, not made for kids) and
|
|
276
|
+
* its validation stay the single source of truth. */
|
|
277
|
+
const youtubeOptions = (flags) => ({
|
|
278
|
+
...flags.youtubePlaylist?.length ? { playlistIds: flags.youtubePlaylist } : {},
|
|
279
|
+
...flags.youtubeTags !== void 0 ? { tags: flags.youtubeTags } : {},
|
|
280
|
+
...flags.youtubeCategory !== void 0 ? { categoryId: flags.youtubeCategory } : {},
|
|
281
|
+
...flags.youtubeLanguage !== void 0 ? { defaultLanguage: flags.youtubeLanguage } : {},
|
|
282
|
+
...flags.youtubeAudioLanguage !== void 0 ? { defaultAudioLanguage: flags.youtubeAudioLanguage } : {},
|
|
283
|
+
...flags.youtubePrivacy !== void 0 ? { privacyStatus: flags.youtubePrivacy } : {},
|
|
284
|
+
...flags.youtubeMadeForKids !== void 0 ? { madeForKids: flags.youtubeMadeForKids } : {},
|
|
285
|
+
...flags.youtubePublishAt !== void 0 ? { publishAt: flags.youtubePublishAt } : {},
|
|
286
|
+
...flags.youtubeSyntheticMedia !== void 0 ? { containsSyntheticMedia: flags.youtubeSyntheticMedia } : {}
|
|
287
|
+
});
|
|
243
288
|
const resolveBrandId = async (ctx, flagBrand) => {
|
|
244
289
|
let brandId = flagBrand ?? ctx.config.brandId;
|
|
245
290
|
if (!brandId) {
|
|
@@ -256,12 +301,17 @@ const resolveBrandId = async (ctx, flagBrand) => {
|
|
|
256
301
|
const uploadAsset = async (ctx, filePath, kind, jsonMode) => {
|
|
257
302
|
const ext = extname(filePath).toLowerCase();
|
|
258
303
|
let contentType = "application/octet-stream";
|
|
259
|
-
if (kind === "
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
else
|
|
264
|
-
|
|
304
|
+
if (kind === "caption") {
|
|
305
|
+
if (ext === ".srt") contentType = "application/x-subrip";
|
|
306
|
+
else if (ext === ".vtt") contentType = "text/vtt";
|
|
307
|
+
else throw new UsageError(`Caption files must be .srt or .vtt: ${filePath}`);
|
|
308
|
+
} else if (kind === "video") {
|
|
309
|
+
if (ext === ".mp4") contentType = "video/mp4";
|
|
310
|
+
else if (ext === ".mov") contentType = "video/quicktime";
|
|
311
|
+
else if (ext === ".m4v") contentType = "video/mp4";
|
|
312
|
+
else if (ext === ".webm") contentType = "video/webm";
|
|
313
|
+
else contentType = "video/mp4";
|
|
314
|
+
} else if (ext === ".jpg" || ext === ".jpeg") contentType = "image/jpeg";
|
|
265
315
|
else if (ext === ".png") contentType = "image/png";
|
|
266
316
|
else if (ext === ".webp") contentType = "image/webp";
|
|
267
317
|
else contentType = "application/octet-stream";
|
|
@@ -315,12 +365,11 @@ const pollPublication = async (ctx, brandId, pubId, jsonMode) => {
|
|
|
315
365
|
while (true) {
|
|
316
366
|
const result = await ctx.api.getPublication(brandId, pubId);
|
|
317
367
|
const pub = result.publication;
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
for (const target of pub.targets) out(ctx, ` - ${target.platform}: ${target.status}${target.error ? ` (${target.error})` : ""}`);
|
|
368
|
+
const log = jsonMode ? err : out;
|
|
369
|
+
log(ctx, `Status: ${pub.status}`);
|
|
370
|
+
for (const target of pub.targets) {
|
|
371
|
+
log(ctx, ` - ${target.platform}: ${target.status}${target.error ? ` (${target.error})` : ""}`);
|
|
372
|
+
for (const warning of target.warnings ?? []) log(ctx, ` warning: ${warning}`);
|
|
324
373
|
}
|
|
325
374
|
if (pub.status === "done" || pub.status === "failed" || pub.status === "partial") return result;
|
|
326
375
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
@@ -336,163 +385,247 @@ const printPublicationSummary = (ctx, pub) => {
|
|
|
336
385
|
out(ctx, `Publication ID: ${pub.id}`);
|
|
337
386
|
out(ctx, `Status: ${pub.status}`);
|
|
338
387
|
out(ctx, "Targets:");
|
|
339
|
-
for (const target of pub.targets)
|
|
388
|
+
for (const target of pub.targets) {
|
|
389
|
+
out(ctx, ` - ${target.platform}: ${target.status}${target.externalUrl ? ` (${target.externalUrl})` : ""}${target.error ? ` - Error: ${target.error}` : ""}`);
|
|
390
|
+
for (const warning of target.warnings ?? []) out(ctx, ` warning: ${warning}`);
|
|
391
|
+
}
|
|
340
392
|
};
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
393
|
+
const createCommand = buildCommand({
|
|
394
|
+
docs: { brief: "Create a publication, uploading local video/cover files as needed" },
|
|
395
|
+
parameters: { flags: {
|
|
396
|
+
video: {
|
|
397
|
+
kind: "parsed",
|
|
398
|
+
parse: String,
|
|
399
|
+
brief: "Video file path or asset id"
|
|
400
|
+
},
|
|
401
|
+
cover: {
|
|
402
|
+
kind: "parsed",
|
|
403
|
+
parse: String,
|
|
404
|
+
optional: true,
|
|
405
|
+
brief: "Cover image path or asset id"
|
|
406
|
+
},
|
|
407
|
+
to: {
|
|
408
|
+
kind: "parsed",
|
|
409
|
+
parse: parseTargets,
|
|
410
|
+
brief: "Comma-separated platforms"
|
|
411
|
+
},
|
|
412
|
+
title: {
|
|
413
|
+
kind: "parsed",
|
|
414
|
+
parse: String,
|
|
415
|
+
brief: "Publication title"
|
|
416
|
+
},
|
|
417
|
+
description: {
|
|
418
|
+
kind: "parsed",
|
|
419
|
+
parse: String,
|
|
420
|
+
optional: true,
|
|
421
|
+
brief: "Publication description"
|
|
422
|
+
},
|
|
423
|
+
youtubeTags: {
|
|
424
|
+
kind: "parsed",
|
|
425
|
+
parse: parseTags,
|
|
426
|
+
optional: true,
|
|
427
|
+
brief: "YouTube: comma-separated tags (500 characters total)"
|
|
428
|
+
},
|
|
429
|
+
youtubeCategory: {
|
|
430
|
+
kind: "parsed",
|
|
431
|
+
parse: String,
|
|
432
|
+
optional: true,
|
|
433
|
+
brief: "YouTube: numeric category id, e.g. 27 (Education)"
|
|
434
|
+
},
|
|
435
|
+
youtubeLanguage: {
|
|
436
|
+
kind: "parsed",
|
|
437
|
+
parse: String,
|
|
438
|
+
optional: true,
|
|
439
|
+
brief: "YouTube: language of the title and description, e.g. en-GB"
|
|
440
|
+
},
|
|
441
|
+
youtubeAudioLanguage: {
|
|
442
|
+
kind: "parsed",
|
|
443
|
+
parse: String,
|
|
444
|
+
optional: true,
|
|
445
|
+
brief: "YouTube: language spoken in the video, e.g. en"
|
|
446
|
+
},
|
|
447
|
+
youtubePrivacy: {
|
|
448
|
+
kind: "enum",
|
|
449
|
+
values: [
|
|
450
|
+
"public",
|
|
451
|
+
"unlisted",
|
|
452
|
+
"private"
|
|
453
|
+
],
|
|
454
|
+
optional: true,
|
|
455
|
+
brief: "YouTube: privacy status (default public)"
|
|
456
|
+
},
|
|
457
|
+
youtubeMadeForKids: {
|
|
458
|
+
kind: "boolean",
|
|
459
|
+
optional: true,
|
|
460
|
+
brief: "YouTube: declare the video made for kids"
|
|
461
|
+
},
|
|
462
|
+
youtubePublishAt: {
|
|
463
|
+
kind: "parsed",
|
|
464
|
+
parse: parseDateTime,
|
|
465
|
+
optional: true,
|
|
466
|
+
brief: "YouTube: upload now as private, go public at this time (local unless offset given)"
|
|
467
|
+
},
|
|
468
|
+
youtubeSyntheticMedia: {
|
|
469
|
+
kind: "boolean",
|
|
470
|
+
optional: true,
|
|
471
|
+
brief: "YouTube: disclose realistic altered or synthetic content"
|
|
472
|
+
},
|
|
473
|
+
youtubeCaption: {
|
|
474
|
+
kind: "parsed",
|
|
475
|
+
parse: parseCaption,
|
|
476
|
+
variadic: true,
|
|
477
|
+
optional: true,
|
|
478
|
+
brief: "YouTube: caption track <lang>[:<name>]=<.srt/.vtt path or asset id> (repeatable)"
|
|
479
|
+
},
|
|
480
|
+
youtubePlaylist: {
|
|
481
|
+
kind: "parsed",
|
|
482
|
+
parse: String,
|
|
483
|
+
variadic: true,
|
|
484
|
+
optional: true,
|
|
485
|
+
brief: "YouTube: add the video to this playlist id (repeatable)"
|
|
486
|
+
},
|
|
487
|
+
brand: {
|
|
488
|
+
kind: "parsed",
|
|
489
|
+
parse: String,
|
|
490
|
+
optional: true,
|
|
491
|
+
brief: "Brand id"
|
|
492
|
+
},
|
|
493
|
+
idempotencyKey: {
|
|
494
|
+
kind: "parsed",
|
|
495
|
+
parse: String,
|
|
496
|
+
optional: true,
|
|
497
|
+
brief: "Idempotency-Key value"
|
|
498
|
+
},
|
|
499
|
+
watch: {
|
|
500
|
+
kind: "boolean",
|
|
501
|
+
brief: "Poll until final status"
|
|
502
|
+
},
|
|
503
|
+
json: {
|
|
504
|
+
kind: "boolean",
|
|
505
|
+
brief: "Print JSON output"
|
|
506
|
+
}
|
|
507
|
+
} },
|
|
508
|
+
async func(flags) {
|
|
509
|
+
requireAuth(this);
|
|
510
|
+
const youtube = youtubeOptions(flags);
|
|
511
|
+
const captionFlags = flags.youtubeCaption ?? [];
|
|
512
|
+
if ((Object.keys(youtube).length > 0 || captionFlags.length > 0) && !flags.to.includes("youtube")) throw new UsageError("`--youtube-*` flags need `youtube` in `--to`.");
|
|
513
|
+
const captions = [];
|
|
514
|
+
for (const { source, ...track } of captionFlags) {
|
|
515
|
+
const assetId = source.startsWith("asset_") ? source : await uploadAsset(this, source, "caption", flags.json);
|
|
516
|
+
captions.push({
|
|
517
|
+
assetId,
|
|
518
|
+
...track
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
if (captions.length > 0) youtube.captions = captions;
|
|
522
|
+
const brandId = await resolveBrandId(this, flags.brand);
|
|
523
|
+
let videoAssetId;
|
|
524
|
+
if (flags.video.startsWith("asset_")) videoAssetId = flags.video;
|
|
525
|
+
else videoAssetId = await uploadAsset(this, flags.video, "video", flags.json);
|
|
526
|
+
let coverAssetId = null;
|
|
527
|
+
if (flags.cover) {
|
|
528
|
+
if (flags.cover.startsWith("asset_")) coverAssetId = flags.cover;
|
|
529
|
+
else coverAssetId = await uploadAsset(this, flags.cover, "cover", flags.json);
|
|
530
|
+
}
|
|
531
|
+
const idempotencyKey = flags.idempotencyKey ?? randomUUID();
|
|
532
|
+
const result = await this.api.createPublication(brandId, {
|
|
533
|
+
videoAssetId,
|
|
534
|
+
coverAssetId,
|
|
535
|
+
title: flags.title,
|
|
536
|
+
description: flags.description ?? "",
|
|
537
|
+
targets: flags.to.map((platform) => ({
|
|
538
|
+
platform,
|
|
539
|
+
options: platform === "youtube" ? youtube : {}
|
|
540
|
+
}))
|
|
541
|
+
}, idempotencyKey);
|
|
542
|
+
if (flags.watch) {
|
|
543
|
+
const finalResult = await pollPublication(this, brandId, result.publicationId, flags.json);
|
|
544
|
+
if (flags.json) return jsonOut(this, finalResult);
|
|
545
|
+
printPublicationSummary(this, finalResult.publication);
|
|
546
|
+
} else {
|
|
547
|
+
if (flags.json) return jsonOut(this, result);
|
|
548
|
+
printDispatchResult(this, result);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
const getCommand = buildCommand({
|
|
553
|
+
docs: { brief: "Read a publication" },
|
|
554
|
+
parameters: {
|
|
555
|
+
flags: {
|
|
556
|
+
brand: {
|
|
557
|
+
kind: "parsed",
|
|
558
|
+
parse: String,
|
|
559
|
+
optional: true,
|
|
560
|
+
brief: "Brand id"
|
|
446
561
|
},
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const result = await this.api.getPublication(brandId, id);
|
|
451
|
-
if (flags.json) return jsonOut(this, result);
|
|
452
|
-
printPublicationSummary(this, result.publication);
|
|
562
|
+
json: {
|
|
563
|
+
kind: "boolean",
|
|
564
|
+
brief: "Print JSON output"
|
|
453
565
|
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
parameters: {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
566
|
+
},
|
|
567
|
+
positional: {
|
|
568
|
+
kind: "tuple",
|
|
569
|
+
parameters: [{
|
|
570
|
+
brief: "Publication id",
|
|
571
|
+
parse: String
|
|
572
|
+
}]
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
async func(flags, id) {
|
|
576
|
+
requireAuth(this);
|
|
577
|
+
const brandId = await resolveBrandId(this, flags.brand);
|
|
578
|
+
const result = await this.api.getPublication(brandId, id);
|
|
579
|
+
if (flags.json) return jsonOut(this, result);
|
|
580
|
+
printPublicationSummary(this, result.publication);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
const statusCommand = buildCommand({
|
|
584
|
+
docs: { brief: "Show publication status" },
|
|
585
|
+
parameters: {
|
|
586
|
+
flags: {
|
|
587
|
+
brand: {
|
|
588
|
+
kind: "parsed",
|
|
589
|
+
parse: String,
|
|
590
|
+
optional: true,
|
|
591
|
+
brief: "Brand id"
|
|
592
|
+
},
|
|
593
|
+
watch: {
|
|
594
|
+
kind: "boolean",
|
|
595
|
+
brief: "Poll until final status"
|
|
481
596
|
},
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (flags.watch) {
|
|
486
|
-
const result = await pollPublication(this, brandId, id, flags.json);
|
|
487
|
-
if (flags.json) return jsonOut(this, result);
|
|
488
|
-
printPublicationSummary(this, result.publication);
|
|
489
|
-
} else {
|
|
490
|
-
const result = await this.api.getPublication(brandId, id);
|
|
491
|
-
if (flags.json) return jsonOut(this, result);
|
|
492
|
-
printPublicationSummary(this, result.publication);
|
|
493
|
-
}
|
|
597
|
+
json: {
|
|
598
|
+
kind: "boolean",
|
|
599
|
+
brief: "Print JSON output"
|
|
494
600
|
}
|
|
495
|
-
}
|
|
601
|
+
},
|
|
602
|
+
positional: {
|
|
603
|
+
kind: "tuple",
|
|
604
|
+
parameters: [{
|
|
605
|
+
brief: "Publication id",
|
|
606
|
+
parse: String
|
|
607
|
+
}]
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
async func(flags, id) {
|
|
611
|
+
requireAuth(this);
|
|
612
|
+
const brandId = await resolveBrandId(this, flags.brand);
|
|
613
|
+
if (flags.watch) {
|
|
614
|
+
const result = await pollPublication(this, brandId, id, flags.json);
|
|
615
|
+
if (flags.json) return jsonOut(this, result);
|
|
616
|
+
printPublicationSummary(this, result.publication);
|
|
617
|
+
} else {
|
|
618
|
+
const result = await this.api.getPublication(brandId, id);
|
|
619
|
+
if (flags.json) return jsonOut(this, result);
|
|
620
|
+
printPublicationSummary(this, result.publication);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
const publicationsRoutes = buildRouteMap({
|
|
625
|
+
routes: {
|
|
626
|
+
create: createCommand,
|
|
627
|
+
get: getCommand,
|
|
628
|
+
status: statusCommand
|
|
496
629
|
},
|
|
497
630
|
docs: { brief: "Create and inspect publications" }
|
|
498
631
|
});
|
|
@@ -504,7 +637,7 @@ const text = {
|
|
|
504
637
|
exceptionWhileRunningCommand: cleanError,
|
|
505
638
|
commandErrorResult: cleanError
|
|
506
639
|
};
|
|
507
|
-
const
|
|
640
|
+
const root = buildRouteMap({
|
|
508
641
|
routes: {
|
|
509
642
|
login: loginCommand,
|
|
510
643
|
logout: logoutCommand,
|
|
@@ -513,7 +646,8 @@ const app = buildApplication(buildRouteMap({
|
|
|
513
646
|
publications: publicationsRoutes
|
|
514
647
|
},
|
|
515
648
|
docs: { brief: "socialized.dev — publish videos from the terminal" }
|
|
516
|
-
})
|
|
649
|
+
});
|
|
650
|
+
const app = buildApplication(root, {
|
|
517
651
|
name: "socialized.dev",
|
|
518
652
|
versionInfo: { currentVersion: version },
|
|
519
653
|
scanner: { caseStyle: "allow-kebab-for-camel" },
|
package/package.json
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@socialized.dev/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"description": "socialized.dev CLI — publish a video once, everywhere, from the terminal.",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/fishballapp/fishballapps.git",
|
|
11
|
-
"directory": "projects/socialized.dev/apps/cli"
|
|
12
|
-
},
|
|
13
8
|
"homepage": "https://socialized.dev",
|
|
14
|
-
"bugs": {
|
|
15
|
-
"url": "https://github.com/fishballapp/fishballapps/issues"
|
|
16
|
-
},
|
|
17
9
|
"bin": {
|
|
18
10
|
"socialized.dev": "./dist/bin.js"
|
|
19
11
|
},
|
|
@@ -25,12 +17,12 @@
|
|
|
25
17
|
"access": "public"
|
|
26
18
|
},
|
|
27
19
|
"dependencies": {
|
|
28
|
-
"@stricli/core": "^1.
|
|
20
|
+
"@stricli/core": "^1.3.0"
|
|
29
21
|
},
|
|
30
22
|
"devDependencies": {
|
|
31
|
-
"@types/node": "^24.
|
|
32
|
-
"tsdown": "^0.
|
|
33
|
-
"vitest": "^4.1.
|
|
23
|
+
"@types/node": "^24.13.4",
|
|
24
|
+
"tsdown": "^0.23.0",
|
|
25
|
+
"vitest": "^4.1.8"
|
|
34
26
|
},
|
|
35
27
|
"scripts": {
|
|
36
28
|
"build": "tsdown",
|