@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 CHANGED
@@ -8,10 +8,10 @@
8
8
  npm install -g @socialized.dev/cli
9
9
  ```
10
10
 
11
- Inside the monorepo:
11
+ This installs the `socialized.dev` command:
12
12
 
13
13
  ```sh
14
- pnpm -F @socialized.dev/cli cli -- --help
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.1.1";
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).then((res) => res.json());
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 { brands } = await createApiClient(resolveConfig({ apiKey: key })).listBrands();
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: buildCommand({
193
- docs: { brief: "List brands available to the API key" },
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 === "video") if (ext === ".mp4") contentType = "video/mp4";
260
- else if (ext === ".mov") contentType = "video/quicktime";
261
- else if (ext === ".m4v") contentType = "video/mp4";
262
- else if (ext === ".webm") contentType = "video/webm";
263
- else contentType = "video/mp4";
264
- else if (ext === ".jpg" || ext === ".jpeg") contentType = "image/jpeg";
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
- if (jsonMode) {
319
- err(ctx, `Status: ${pub.status}`);
320
- for (const target of pub.targets) err(ctx, ` - ${target.platform}: ${target.status}${target.error ? ` (${target.error})` : ""}`);
321
- } else {
322
- out(ctx, `Status: ${pub.status}`);
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) out(ctx, ` - ${target.platform}: ${target.status}${target.externalUrl ? ` (${target.externalUrl})` : ""}${target.error ? ` - Error: ${target.error}` : ""}`);
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 publicationsRoutes = buildRouteMap({
342
- routes: {
343
- create: buildCommand({
344
- docs: { brief: "Create a publication, uploading local video/cover files as needed" },
345
- parameters: { flags: {
346
- video: {
347
- kind: "parsed",
348
- parse: String,
349
- brief: "Video file path or asset id"
350
- },
351
- cover: {
352
- kind: "parsed",
353
- parse: String,
354
- optional: true,
355
- brief: "Cover image path or asset id"
356
- },
357
- to: {
358
- kind: "parsed",
359
- parse: parseTargets,
360
- brief: "Comma-separated platforms"
361
- },
362
- title: {
363
- kind: "parsed",
364
- parse: String,
365
- brief: "Publication title"
366
- },
367
- description: {
368
- kind: "parsed",
369
- parse: String,
370
- optional: true,
371
- brief: "Publication description"
372
- },
373
- brand: {
374
- kind: "parsed",
375
- parse: String,
376
- optional: true,
377
- brief: "Brand id"
378
- },
379
- idempotencyKey: {
380
- kind: "parsed",
381
- parse: String,
382
- optional: true,
383
- brief: "Idempotency-Key value"
384
- },
385
- watch: {
386
- kind: "boolean",
387
- brief: "Poll until final status"
388
- },
389
- json: {
390
- kind: "boolean",
391
- brief: "Print JSON output"
392
- }
393
- } },
394
- async func(flags) {
395
- requireAuth(this);
396
- const brandId = await resolveBrandId(this, flags.brand);
397
- let videoAssetId;
398
- if (flags.video.startsWith("asset_")) videoAssetId = flags.video;
399
- else videoAssetId = await uploadAsset(this, flags.video, "video", flags.json);
400
- let coverAssetId = null;
401
- if (flags.cover) if (flags.cover.startsWith("asset_")) coverAssetId = flags.cover;
402
- else coverAssetId = await uploadAsset(this, flags.cover, "cover", flags.json);
403
- const idempotencyKey = flags.idempotencyKey ?? randomUUID();
404
- const result = await this.api.createPublication(brandId, {
405
- videoAssetId,
406
- coverAssetId,
407
- title: flags.title,
408
- description: flags.description ?? "",
409
- targets: flags.to.map((platform) => ({
410
- platform,
411
- options: {}
412
- }))
413
- }, idempotencyKey);
414
- if (flags.watch) {
415
- const finalResult = await pollPublication(this, brandId, result.publicationId, flags.json);
416
- if (flags.json) return jsonOut(this, finalResult);
417
- printPublicationSummary(this, finalResult.publication);
418
- } else {
419
- if (flags.json) return jsonOut(this, result);
420
- printDispatchResult(this, result);
421
- }
422
- }
423
- }),
424
- get: buildCommand({
425
- docs: { brief: "Read a publication" },
426
- parameters: {
427
- flags: {
428
- brand: {
429
- kind: "parsed",
430
- parse: String,
431
- optional: true,
432
- brief: "Brand id"
433
- },
434
- json: {
435
- kind: "boolean",
436
- brief: "Print JSON output"
437
- }
438
- },
439
- positional: {
440
- kind: "tuple",
441
- parameters: [{
442
- brief: "Publication id",
443
- parse: String
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
- async func(flags, id) {
448
- requireAuth(this);
449
- const brandId = await resolveBrandId(this, flags.brand);
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
- status: buildCommand({
456
- docs: { brief: "Show publication status" },
457
- parameters: {
458
- flags: {
459
- brand: {
460
- kind: "parsed",
461
- parse: String,
462
- optional: true,
463
- brief: "Brand id"
464
- },
465
- watch: {
466
- kind: "boolean",
467
- brief: "Poll until final status"
468
- },
469
- json: {
470
- kind: "boolean",
471
- brief: "Print JSON output"
472
- }
473
- },
474
- positional: {
475
- kind: "tuple",
476
- parameters: [{
477
- brief: "Publication id",
478
- parse: String
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
- async func(flags, id) {
483
- requireAuth(this);
484
- const brandId = await resolveBrandId(this, flags.brand);
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 app = buildApplication(buildRouteMap({
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.1.1",
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.2.7"
20
+ "@stricli/core": "^1.3.0"
29
21
  },
30
22
  "devDependencies": {
31
- "@types/node": "^24.0.0",
32
- "tsdown": "^0.22.3",
33
- "vitest": "^4.1.5"
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",