@socialized.dev/cli 0.1.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 ADDED
@@ -0,0 +1,66 @@
1
+ # @socialized.dev/cli
2
+
3
+ `socialized.dev` publishes videos through the socialized.dev API from a terminal or CI job.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install -g @socialized.dev/cli
9
+ ```
10
+
11
+ Inside the monorepo:
12
+
13
+ ```sh
14
+ pnpm -F @socialized.dev/cli cli -- --help
15
+ ```
16
+
17
+ ## Auth
18
+
19
+ Create an API key in the dashboard, then store it locally:
20
+
21
+ ```sh
22
+ socialized.dev login --key sk_...
23
+ socialized.dev whoami
24
+ ```
25
+
26
+ Headless runs can use environment variables instead of stored config:
27
+
28
+ ```sh
29
+ SOCIALIZED_DEV_API_KEY=sk_... socialized.dev brands list
30
+ ```
31
+
32
+ ## Publications
33
+
34
+ ```sh
35
+ socialized.dev publications create \
36
+ --video ./clip.mp4 \
37
+ --to youtube,instagram \
38
+ --title "Launch clip" \
39
+ --description "Posted once, everywhere." \
40
+ --watch
41
+ ```
42
+
43
+ `publications create` uploads local video and cover files internally, then creates the
44
+ publication. You can also pass an existing `asset_...` id to `--video` or `--cover`.
45
+
46
+ ## Brands
47
+
48
+ If your account has multiple brands, you can switch between them:
49
+
50
+ ```sh
51
+ # List all brands
52
+ socialized.dev brands list
53
+
54
+ # Select a brand for future publication commands
55
+ socialized.dev brands use brand_...
56
+ ```
57
+
58
+ ## Environment Variables
59
+
60
+ - `SOCIALIZED_DEV_API_KEY`: API Key to authenticate requests.
61
+ - `SOCIALIZED_DEV_BRAND_ID`: Explicitly select a brand ID for commands.
62
+ - `SOCIALIZED_DEV_API_URL`: Use a custom API host (default is `https://api.socialized.dev/api/v1`).
63
+
64
+ ## CI/CD and npm Trusted Publishing
65
+
66
+ This package is securely published to npm registry using **npm OIDC Trusted Publishing** via GitHub Actions. Trusted publishing allows publishing directly from a verified GitHub Actions workflow without managing or leaking npm automation tokens.
package/dist/bin.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/bin.js ADDED
@@ -0,0 +1,531 @@
1
+ #!/usr/bin/env node
2
+ import { buildApplication, buildCommand, buildRouteMap, run, text_en } from "@stricli/core";
3
+ import { createReadStream, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { basename, extname, join } from "node:path";
6
+ import { randomUUID } from "node:crypto";
7
+ import { Readable } from "node:stream";
8
+ //#region package.json
9
+ var version = "0.1.0";
10
+ //#endregion
11
+ //#region src/api.ts
12
+ var ApiError = class extends Error {
13
+ status;
14
+ body;
15
+ constructor(status, body, method, path) {
16
+ super(`socialized.dev API ${method} ${path} failed (${status}): ${formatErrorBody(body)}`);
17
+ this.status = status;
18
+ this.body = body;
19
+ }
20
+ };
21
+ const formatErrorBody = (body) => {
22
+ try {
23
+ const parsed = JSON.parse(body);
24
+ if (parsed !== null && typeof parsed === "object") {
25
+ const record = parsed;
26
+ const code = typeof record.error === "string" ? record.error : void 0;
27
+ const message = typeof record.message === "string" ? record.message : void 0;
28
+ if (code && message) return `${code}: ${message}`;
29
+ if (message) return message;
30
+ if (code) return code;
31
+ }
32
+ } catch {}
33
+ return body || "(empty response)";
34
+ };
35
+ const createApiClient = (config) => {
36
+ const request = async (path, init = {}) => {
37
+ const res = await fetch(`${config.apiUrl}${path}`, {
38
+ ...init,
39
+ headers: {
40
+ ...config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
41
+ ...init.body ? { "content-type": "application/json" } : {},
42
+ ...init.headers
43
+ }
44
+ });
45
+ if (!res.ok) throw new ApiError(res.status, await res.text(), init.method ?? "GET", path);
46
+ return res;
47
+ };
48
+ const json = (path, init) => request(path, init).then((res) => res.json());
49
+ return {
50
+ listBrands: () => json("/brands"),
51
+ startAssetUpload: (body) => json("/assets", {
52
+ method: "POST",
53
+ body: JSON.stringify(body)
54
+ }),
55
+ completeAssetUpload: (uploadId, parts) => json(`/assets/${encodeURIComponent(uploadId)}/complete`, {
56
+ method: "POST",
57
+ body: JSON.stringify({ parts })
58
+ }),
59
+ createPublication: (brandId, body, idempotencyKey) => json(`/brands/${encodeURIComponent(brandId)}/publications`, {
60
+ method: "POST",
61
+ headers: { "Idempotency-Key": idempotencyKey },
62
+ body: JSON.stringify(body)
63
+ }),
64
+ getPublication: (brandId, publicationId) => json(`/brands/${encodeURIComponent(brandId)}/publications/${encodeURIComponent(publicationId)}`)
65
+ };
66
+ };
67
+ //#endregion
68
+ //#region src/config.ts
69
+ const getConfigDir = () => join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "socialized.dev");
70
+ const getConfigFile = () => join(getConfigDir(), "config.json");
71
+ const DEFAULT_API_URL = "https://api.socialized.dev/api/v1";
72
+ const readStored = () => {
73
+ try {
74
+ return JSON.parse(readFileSync(getConfigFile(), "utf8"));
75
+ } catch {
76
+ return {};
77
+ }
78
+ };
79
+ const writeStored = (config) => {
80
+ mkdirSync(getConfigDir(), { recursive: true });
81
+ writeFileSync(getConfigFile(), `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
82
+ };
83
+ const saveConfig = (patch) => writeStored({
84
+ ...readStored(),
85
+ ...patch
86
+ });
87
+ const clearConfig = () => writeStored({});
88
+ const resolveConfig = (overrides = {}) => {
89
+ const stored = readStored();
90
+ return {
91
+ apiKey: overrides.apiKey ?? process.env.SOCIALIZED_DEV_API_KEY ?? stored.apiKey ?? null,
92
+ apiUrl: overrides.apiUrl ?? process.env.SOCIALIZED_DEV_API_URL ?? stored.apiUrl ?? DEFAULT_API_URL,
93
+ brandId: overrides.brandId ?? process.env.SOCIALIZED_DEV_BRAND_ID ?? stored.brandId ?? null
94
+ };
95
+ };
96
+ //#endregion
97
+ //#region src/util.ts
98
+ var UsageError = class extends Error {};
99
+ const readStdin = async () => {
100
+ const chunks = [];
101
+ for await (const chunk of process.stdin) chunks.push(chunk);
102
+ return Buffer.concat(chunks).toString("utf8");
103
+ };
104
+ //#endregion
105
+ //#region src/context.ts
106
+ const buildContext = () => {
107
+ const config = resolveConfig();
108
+ return {
109
+ process,
110
+ config,
111
+ api: createApiClient(config)
112
+ };
113
+ };
114
+ const requireAuth = (ctx) => {
115
+ if (!ctx.config.apiKey) throw new UsageError("Not authenticated. Run `socialized.dev login --key sk_...` first.");
116
+ };
117
+ //#endregion
118
+ //#region src/output.ts
119
+ const out = (ctx, message) => {
120
+ ctx.process.stdout.write(`${message}\n`);
121
+ };
122
+ const err = (ctx, message) => {
123
+ ctx.process.stderr.write(`${message}\n`);
124
+ };
125
+ const jsonOut = (ctx, value) => {
126
+ out(ctx, JSON.stringify(value, null, 2));
127
+ };
128
+ //#endregion
129
+ //#region src/commands/auth.ts
130
+ const loginCommand = buildCommand({
131
+ docs: { brief: "Store an API key for socialized.dev" },
132
+ parameters: { flags: { key: {
133
+ kind: "parsed",
134
+ parse: String,
135
+ optional: true,
136
+ brief: "API key; or pipe it: `echo sk_... | socialized.dev login`"
137
+ } } },
138
+ async func(flags) {
139
+ const piped = flags.key === void 0 && !process.stdin.isTTY ? (await readStdin()).trim() : void 0;
140
+ const key = (flags.key ?? piped)?.trim();
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();
143
+ saveConfig({
144
+ apiKey: key,
145
+ brandId: brands.length === 1 ? brands[0]?.id : void 0
146
+ });
147
+ out(this, `Authenticated. ${brands.length} brand${brands.length === 1 ? "" : "s"} available.`);
148
+ }
149
+ });
150
+ const logoutCommand = buildCommand({
151
+ docs: { brief: "Clear stored socialized.dev config" },
152
+ parameters: {},
153
+ func() {
154
+ clearConfig();
155
+ out(this, "Logged out.");
156
+ }
157
+ });
158
+ const whoamiCommand = buildCommand({
159
+ docs: { brief: "Show API URL, selected brand, and connected platforms" },
160
+ parameters: { flags: { json: {
161
+ kind: "boolean",
162
+ brief: "Print JSON output"
163
+ } } },
164
+ async func(flags) {
165
+ requireAuth(this);
166
+ const { brands } = await this.api.listBrands();
167
+ const selected = brands.find((brand) => brand.id === this.config.brandId) ?? brands[0] ?? null;
168
+ if (flags.json) return jsonOut(this, {
169
+ apiUrl: this.config.apiUrl,
170
+ selectedBrand: selected,
171
+ brands
172
+ });
173
+ out(this, `API: ${this.config.apiUrl}`);
174
+ out(this, `Brand: ${selected ? `${selected.name} (${selected.id})` : "(none)"}`);
175
+ if (selected) out(this, `Connected: ${selected.connectedPlatforms.join(", ") || "(none)"}`);
176
+ }
177
+ });
178
+ //#endregion
179
+ //#region src/commands/brands.ts
180
+ const resolveBrand = (brands, selector) => {
181
+ const matches = brands.filter((brand) => brand.id === selector || brand.name === selector);
182
+ if (matches.length === 1) {
183
+ const brand = matches[0];
184
+ if (brand === void 0) throw new UsageError(`Brand not found: ${selector}`);
185
+ return brand;
186
+ }
187
+ if (matches.length > 1) throw new UsageError(`Brand name is ambiguous: ${selector}`);
188
+ throw new UsageError(`Brand not found: ${selector}`);
189
+ };
190
+ const brandsRoutes = buildRouteMap({
191
+ 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
+ })
227
+ },
228
+ docs: { brief: "Manage default brand selection" }
229
+ });
230
+ //#endregion
231
+ //#region src/commands/publications.ts
232
+ const parseTargets = (value) => {
233
+ const targets = value.split(",").map((target) => target.trim()).filter(Boolean);
234
+ if (targets.length === 0) throw new UsageError("Pass at least one platform with `--to`.");
235
+ const seen = /* @__PURE__ */ new Set();
236
+ for (const target of targets) {
237
+ if (target !== "youtube" && target !== "instagram" && target !== "facebook" && target !== "tiktok") throw new UsageError(`Unsupported platform: ${target}`);
238
+ if (seen.has(target)) throw new UsageError(`Duplicate platform not allowed: ${target}`);
239
+ seen.add(target);
240
+ }
241
+ return targets;
242
+ };
243
+ const resolveBrandId = async (ctx, flagBrand) => {
244
+ let brandId = flagBrand ?? ctx.config.brandId;
245
+ if (!brandId) {
246
+ const { brands } = await ctx.api.listBrands();
247
+ if (brands.length === 1) {
248
+ const firstBrand = brands[0];
249
+ if (firstBrand) brandId = firstBrand.id;
250
+ else throw new UsageError("No brands found. Go to the web dashboard to create one.");
251
+ } else if (brands.length === 0) throw new UsageError("No brands found. Go to the web dashboard to create one.");
252
+ else throw new UsageError("Multiple brands available. Select a default brand with `brands use <brandId>`, or pass `--brand <brandId>`.");
253
+ }
254
+ return brandId;
255
+ };
256
+ const uploadAsset = async (ctx, filePath, kind, jsonMode) => {
257
+ const ext = extname(filePath).toLowerCase();
258
+ 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";
265
+ else if (ext === ".png") contentType = "image/png";
266
+ else if (ext === ".webp") contentType = "image/webp";
267
+ else contentType = "application/octet-stream";
268
+ const stat = statSync(filePath);
269
+ if (!stat.isFile()) throw new UsageError(`Not a file: ${filePath}`);
270
+ if (stat.size <= 0) throw new UsageError(`File is empty: ${filePath}`);
271
+ const filename = basename(filePath);
272
+ const sizeBytes = stat.size;
273
+ const log = (msg) => {
274
+ if (jsonMode) err(ctx, msg);
275
+ else out(ctx, msg);
276
+ };
277
+ log(`Uploading ${kind} file: ${filename} (${sizeBytes} bytes, MIME: ${contentType})...`);
278
+ const plan = await ctx.api.startAssetUpload({
279
+ filename,
280
+ contentType,
281
+ sizeBytes
282
+ });
283
+ const completedParts = [];
284
+ for (const part of plan.parts) {
285
+ const partNumber = part.partNumber;
286
+ const start = (partNumber - 1) * plan.partSize;
287
+ const end = Math.min(sizeBytes, partNumber * plan.partSize) - 1;
288
+ log(` Uploading part ${partNumber}/${plan.parts.length} (bytes ${start}-${end})...`);
289
+ const stream = createReadStream(filePath, {
290
+ start,
291
+ end
292
+ });
293
+ const uploadInit = {
294
+ method: "PUT",
295
+ body: Readable.toWeb(stream),
296
+ duplex: "half",
297
+ headers: { "Content-Length": String(end - start + 1) }
298
+ };
299
+ const response = await fetch(part.url, uploadInit);
300
+ if (!response.ok) throw new Error(`Failed to upload part ${partNumber}: HTTP ${response.status}`);
301
+ const etag = response.headers.get("etag");
302
+ if (!etag) throw new Error(`Upload of part ${partNumber} failed: missing ETag header`);
303
+ completedParts.push({
304
+ partNumber,
305
+ etag
306
+ });
307
+ }
308
+ log(`Completing multipart upload...`);
309
+ const completeResult = await ctx.api.completeAssetUpload(plan.uploadId, completedParts);
310
+ log(`Asset uploaded successfully: ${completeResult.assetId}`);
311
+ return completeResult.assetId;
312
+ };
313
+ const pollPublication = async (ctx, brandId, pubId, jsonMode) => {
314
+ const intervalMs = 2e3;
315
+ while (true) {
316
+ const result = await ctx.api.getPublication(brandId, pubId);
317
+ 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})` : ""}`);
324
+ }
325
+ if (pub.status === "done" || pub.status === "failed" || pub.status === "partial") return result;
326
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
327
+ }
328
+ };
329
+ const printDispatchResult = (ctx, res) => {
330
+ out(ctx, `Publication queued. ID: ${res.publicationId}`);
331
+ out(ctx, `Status: ${res.status}`);
332
+ out(ctx, "Targets:");
333
+ for (const target of res.targets) out(ctx, ` - ${target.platform}: ${target.status}`);
334
+ };
335
+ const printPublicationSummary = (ctx, pub) => {
336
+ out(ctx, `Publication ID: ${pub.id}`);
337
+ out(ctx, `Status: ${pub.status}`);
338
+ 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}` : ""}`);
340
+ };
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
+ }
446
+ },
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);
453
+ }
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
+ }
481
+ },
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
+ }
494
+ }
495
+ })
496
+ },
497
+ docs: { brief: "Create and inspect publications" }
498
+ });
499
+ //#endregion
500
+ //#region src/app.ts
501
+ const cleanError = (error) => error instanceof Error ? error.message : String(error);
502
+ const text = {
503
+ ...text_en,
504
+ exceptionWhileRunningCommand: cleanError,
505
+ commandErrorResult: cleanError
506
+ };
507
+ const app = buildApplication(buildRouteMap({
508
+ routes: {
509
+ login: loginCommand,
510
+ logout: logoutCommand,
511
+ whoami: whoamiCommand,
512
+ brands: brandsRoutes,
513
+ publications: publicationsRoutes
514
+ },
515
+ docs: { brief: "socialized.dev — publish videos from the terminal" }
516
+ }), {
517
+ name: "socialized.dev",
518
+ versionInfo: { currentVersion: version },
519
+ scanner: { caseStyle: "allow-kebab-for-camel" },
520
+ localization: { loadText: () => text }
521
+ });
522
+ //#endregion
523
+ //#region src/bin.ts
524
+ try {
525
+ await run(app, process.argv.slice(2), buildContext());
526
+ } catch (error) {
527
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
528
+ process.exitCode = 1;
529
+ }
530
+ //#endregion
531
+ export {};
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@socialized.dev/cli",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "license": "MIT",
6
+ "type": "module",
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
+ "homepage": "https://socialized.dev",
14
+ "bugs": {
15
+ "url": "https://github.com/fishballapp/fishballapps/issues"
16
+ },
17
+ "bin": {
18
+ "socialized.dev": "./dist/bin.js"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "dependencies": {
28
+ "@stricli/core": "^1.2.7"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^24.0.0",
32
+ "tsdown": "^0.22.3",
33
+ "vitest": "^4.1.5"
34
+ },
35
+ "scripts": {
36
+ "build": "tsdown",
37
+ "cli": "pnpm build && node dist/bin.js",
38
+ "test": "vitest run --passWithNoTests"
39
+ }
40
+ }