@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 +66 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +531 -0
- package/package.json +40 -0
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
|
+
}
|