@officexapp/catalogs-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 +92 -0
- package/dist/index.js +408 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @officexapp/catalogs-cli
|
|
2
|
+
|
|
3
|
+
CLI for Catalog Funnel — upload videos, push catalog schemas, manage assets.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @officexapp/catalogs-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Configure authentication:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
export CATALOGS_API_URL="https://catalog-funnel-api-staging.cloud.zoomgtm.com"
|
|
15
|
+
export CATALOGS_TOKEN="cfk_your_api_key_here"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or create `~/.catalogs-cli/config.json`:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"api_url": "https://catalog-funnel-api-staging.cloud.zoomgtm.com",
|
|
23
|
+
"token": "cfk_your_api_key_here"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
### Video
|
|
30
|
+
|
|
31
|
+
Upload a video, transcode to HLS, and get the streaming URL:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
catalogs video upload ./demo.mp4
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Skip transcoding (upload only):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
catalogs video upload ./demo.mp4 --no-transcode
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Check transcode status:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
catalogs video status <videoId>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Catalog
|
|
50
|
+
|
|
51
|
+
Push a catalog schema (creates or updates):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
catalogs catalog push ./my-funnel.json
|
|
55
|
+
catalogs catalog push ./my-funnel.json --publish
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
List all catalogs:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
catalogs catalog list
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Auth
|
|
65
|
+
|
|
66
|
+
Check your current config:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
catalogs whoami
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Using HLS URLs in Catalog Schemas
|
|
73
|
+
|
|
74
|
+
After uploading a video, use the returned `hls_url` in your catalog schema:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"id": "comp_intro_video",
|
|
79
|
+
"type": "video",
|
|
80
|
+
"props": {
|
|
81
|
+
"hls_url": "https://d1k9qtz75bfygl.cloudfront.net/media/transcoded/user123/video456/index.m3u8",
|
|
82
|
+
"poster": "https://example.com/thumb.jpg",
|
|
83
|
+
"chapters": [
|
|
84
|
+
{ "time": 0, "label": "Intro" },
|
|
85
|
+
{ "time": 120, "label": "Demo" },
|
|
86
|
+
{ "time": 300, "label": "Pricing" }
|
|
87
|
+
],
|
|
88
|
+
"skippable": false,
|
|
89
|
+
"require_watch_percent": 80
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/video-upload.ts
|
|
7
|
+
import { readFileSync as readFileSync2, statSync } from "fs";
|
|
8
|
+
import { basename } from "path";
|
|
9
|
+
import ora from "ora";
|
|
10
|
+
|
|
11
|
+
// src/config.ts
|
|
12
|
+
import { readFileSync, existsSync } from "fs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
var CONFIG_DIR = join(homedir(), ".catalogs-cli");
|
|
16
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
17
|
+
function getConfig() {
|
|
18
|
+
const envUrl = process.env.CATALOGS_API_URL;
|
|
19
|
+
const envToken = process.env.CATALOGS_TOKEN;
|
|
20
|
+
if (envUrl && envToken) {
|
|
21
|
+
return { api_url: envUrl, token: envToken };
|
|
22
|
+
}
|
|
23
|
+
if (existsSync(CONFIG_FILE)) {
|
|
24
|
+
try {
|
|
25
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
return {
|
|
28
|
+
api_url: envUrl || parsed.api_url || "",
|
|
29
|
+
token: envToken || parsed.token || ""
|
|
30
|
+
};
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const dotenv = join(process.cwd(), ".env");
|
|
35
|
+
if (existsSync(dotenv)) {
|
|
36
|
+
const lines = readFileSync(dotenv, "utf-8").split("\n");
|
|
37
|
+
const env = {};
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
const match = line.match(/^([A-Z_]+)=["']?(.+?)["']?\s*$/);
|
|
40
|
+
if (match) env[match[1]] = match[2];
|
|
41
|
+
}
|
|
42
|
+
if (env.CATALOGS_API_URL || env.CATALOGS_TOKEN) {
|
|
43
|
+
return {
|
|
44
|
+
api_url: envUrl || env.CATALOGS_API_URL || "",
|
|
45
|
+
token: envToken || env.CATALOGS_TOKEN || ""
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
api_url: envUrl || "",
|
|
51
|
+
token: envToken || ""
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function requireConfig() {
|
|
55
|
+
const config = getConfig();
|
|
56
|
+
if (!config.api_url) {
|
|
57
|
+
console.error(
|
|
58
|
+
"Missing API URL. Set CATALOGS_API_URL env var or create ~/.catalogs-cli/config.json"
|
|
59
|
+
);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
if (!config.token) {
|
|
63
|
+
console.error(
|
|
64
|
+
"Missing auth token. Set CATALOGS_TOKEN env var or create ~/.catalogs-cli/config.json"
|
|
65
|
+
);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
return config;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/api.ts
|
|
72
|
+
var ApiClient = class {
|
|
73
|
+
constructor(config) {
|
|
74
|
+
this.config = config;
|
|
75
|
+
}
|
|
76
|
+
get headers() {
|
|
77
|
+
return {
|
|
78
|
+
Authorization: `Bearer ${this.config.token}`,
|
|
79
|
+
"Content-Type": "application/json"
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async get(path) {
|
|
83
|
+
const res = await fetch(`${this.config.api_url}${path}`, {
|
|
84
|
+
method: "GET",
|
|
85
|
+
headers: this.headers
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
const body = await res.text();
|
|
89
|
+
throw new Error(`GET ${path} failed (${res.status}): ${body}`);
|
|
90
|
+
}
|
|
91
|
+
return res.json();
|
|
92
|
+
}
|
|
93
|
+
async post(path, body) {
|
|
94
|
+
const res = await fetch(`${this.config.api_url}${path}`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: this.headers,
|
|
97
|
+
body: body ? JSON.stringify(body) : void 0
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
const text = await res.text();
|
|
101
|
+
throw new Error(`POST ${path} failed (${res.status}): ${text}`);
|
|
102
|
+
}
|
|
103
|
+
return res.json();
|
|
104
|
+
}
|
|
105
|
+
async put(path, body) {
|
|
106
|
+
const res = await fetch(`${this.config.api_url}${path}`, {
|
|
107
|
+
method: "PUT",
|
|
108
|
+
headers: this.headers,
|
|
109
|
+
body: body ? JSON.stringify(body) : void 0
|
|
110
|
+
});
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
const text = await res.text();
|
|
113
|
+
throw new Error(`PUT ${path} failed (${res.status}): ${text}`);
|
|
114
|
+
}
|
|
115
|
+
return res.json();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/commands/video-upload.ts
|
|
120
|
+
var MIME_MAP = {
|
|
121
|
+
".mp4": "video/mp4",
|
|
122
|
+
".mov": "video/quicktime",
|
|
123
|
+
".avi": "video/x-msvideo",
|
|
124
|
+
".mkv": "video/x-matroska",
|
|
125
|
+
".webm": "video/webm",
|
|
126
|
+
".m4v": "video/x-m4v",
|
|
127
|
+
".wmv": "video/x-ms-wmv",
|
|
128
|
+
".flv": "video/x-flv"
|
|
129
|
+
};
|
|
130
|
+
function getContentType(filename) {
|
|
131
|
+
const ext = filename.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
|
|
132
|
+
return MIME_MAP[ext] || "video/mp4";
|
|
133
|
+
}
|
|
134
|
+
function formatBytes(bytes) {
|
|
135
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
136
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
137
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
138
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
139
|
+
}
|
|
140
|
+
async function videoUpload(file, opts) {
|
|
141
|
+
const config = requireConfig();
|
|
142
|
+
const api = new ApiClient(config);
|
|
143
|
+
const skipTranscode = opts.transcode === false;
|
|
144
|
+
let stat;
|
|
145
|
+
try {
|
|
146
|
+
stat = statSync(file);
|
|
147
|
+
} catch {
|
|
148
|
+
console.error(`File not found: ${file}`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
const filename = basename(file);
|
|
152
|
+
const contentType = getContentType(filename);
|
|
153
|
+
const sizeBytes = stat.size;
|
|
154
|
+
console.log(`
|
|
155
|
+
File: ${filename} (${formatBytes(sizeBytes)})`);
|
|
156
|
+
console.log(`Type: ${contentType}
|
|
157
|
+
`);
|
|
158
|
+
const spinner = ora("Requesting upload URL...").start();
|
|
159
|
+
let videoId;
|
|
160
|
+
let uploadUrl;
|
|
161
|
+
try {
|
|
162
|
+
const res = await api.post("/api/v1/videos/upload", {
|
|
163
|
+
filename,
|
|
164
|
+
content_type: contentType,
|
|
165
|
+
size_bytes: sizeBytes
|
|
166
|
+
});
|
|
167
|
+
videoId = res.data.video_id;
|
|
168
|
+
uploadUrl = res.data.upload_url;
|
|
169
|
+
spinner.succeed(
|
|
170
|
+
`Upload authorized \u2014 video_id: ${videoId} (${res.data.credits_charged} credits)`
|
|
171
|
+
);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
spinner.fail(`Upload request failed: ${err.message}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
const uploadSpinner = ora("Uploading to S3...").start();
|
|
177
|
+
try {
|
|
178
|
+
const fileBuffer = readFileSync2(file);
|
|
179
|
+
const res = await fetch(uploadUrl, {
|
|
180
|
+
method: "PUT",
|
|
181
|
+
headers: { "Content-Type": contentType },
|
|
182
|
+
body: fileBuffer
|
|
183
|
+
});
|
|
184
|
+
if (!res.ok) {
|
|
185
|
+
throw new Error(`S3 PUT failed: ${res.status} ${res.statusText}`);
|
|
186
|
+
}
|
|
187
|
+
uploadSpinner.succeed("Upload complete");
|
|
188
|
+
} catch (err) {
|
|
189
|
+
uploadSpinner.fail(`Upload failed: ${err.message}`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
if (skipTranscode) {
|
|
193
|
+
console.log(`
|
|
194
|
+
video_id: ${videoId}`);
|
|
195
|
+
console.log("Skipped transcoding. Run `catalogs video status` to check later.\n");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const transcodeSpinner = ora("Starting HLS transcode...").start();
|
|
199
|
+
try {
|
|
200
|
+
const res = await api.post(`/api/v1/videos/${videoId}/transcode`);
|
|
201
|
+
transcodeSpinner.succeed(
|
|
202
|
+
`Transcode started \u2014 job_id: ${res.data.job_id} (${res.data.credits_charged} credits)`
|
|
203
|
+
);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
transcodeSpinner.fail(`Transcode request failed: ${err.message}`);
|
|
206
|
+
console.log(`
|
|
207
|
+
video_id: ${videoId}`);
|
|
208
|
+
console.log("Upload succeeded but transcode failed. Retry with: catalogs video status\n");
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
const pollSpinner = ora("Transcoding (this may take a few minutes)...").start();
|
|
212
|
+
const startTime = Date.now();
|
|
213
|
+
const MAX_POLL = 10 * 60 * 1e3;
|
|
214
|
+
const POLL_INTERVAL = 5e3;
|
|
215
|
+
while (Date.now() - startTime < MAX_POLL) {
|
|
216
|
+
await sleep(POLL_INTERVAL);
|
|
217
|
+
try {
|
|
218
|
+
const res = await api.get(`/api/v1/videos/${videoId}/status`);
|
|
219
|
+
const { status, hls_url } = res.data;
|
|
220
|
+
if (status === "ready" && hls_url) {
|
|
221
|
+
pollSpinner.succeed("Transcode complete!");
|
|
222
|
+
console.log(`
|
|
223
|
+
video_id: ${videoId}`);
|
|
224
|
+
console.log(` hls_url: ${hls_url}`);
|
|
225
|
+
console.log(
|
|
226
|
+
`
|
|
227
|
+
Add to your catalog schema:
|
|
228
|
+
{ "type": "video", "props": { "hls_url": "${hls_url}" } }
|
|
229
|
+
`
|
|
230
|
+
);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (status === "failed") {
|
|
234
|
+
pollSpinner.fail("Transcode failed");
|
|
235
|
+
console.log(`
|
|
236
|
+
video_id: ${videoId}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
const elapsed = Math.round((Date.now() - startTime) / 1e3);
|
|
240
|
+
pollSpinner.text = `Transcoding... (${elapsed}s)`;
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
pollSpinner.warn("Transcode still in progress (timed out waiting)");
|
|
245
|
+
console.log(`
|
|
246
|
+
Check status later: catalogs video status ${videoId}
|
|
247
|
+
`);
|
|
248
|
+
}
|
|
249
|
+
function sleep(ms) {
|
|
250
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/commands/video-status.ts
|
|
254
|
+
import ora2 from "ora";
|
|
255
|
+
async function videoStatus(videoId) {
|
|
256
|
+
const config = requireConfig();
|
|
257
|
+
const api = new ApiClient(config);
|
|
258
|
+
const spinner = ora2(`Checking status for ${videoId}...`).start();
|
|
259
|
+
try {
|
|
260
|
+
const res = await api.get(`/api/v1/videos/${videoId}/status`);
|
|
261
|
+
const { status, hls_url, filename, size_bytes } = res.data;
|
|
262
|
+
spinner.stop();
|
|
263
|
+
console.log(`
|
|
264
|
+
video_id: ${videoId}`);
|
|
265
|
+
console.log(` filename: ${filename}`);
|
|
266
|
+
if (size_bytes) {
|
|
267
|
+
const mb = (size_bytes / (1024 * 1024)).toFixed(1);
|
|
268
|
+
console.log(` size: ${mb} MB`);
|
|
269
|
+
}
|
|
270
|
+
console.log(` status: ${status}`);
|
|
271
|
+
if (hls_url) {
|
|
272
|
+
console.log(` hls_url: ${hls_url}`);
|
|
273
|
+
}
|
|
274
|
+
console.log();
|
|
275
|
+
} catch (err) {
|
|
276
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/commands/catalog-push.ts
|
|
282
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
283
|
+
import ora3 from "ora";
|
|
284
|
+
async function catalogPush(file, opts) {
|
|
285
|
+
const config = requireConfig();
|
|
286
|
+
const api = new ApiClient(config);
|
|
287
|
+
let schema;
|
|
288
|
+
try {
|
|
289
|
+
const raw = readFileSync3(file, "utf-8");
|
|
290
|
+
schema = JSON.parse(raw);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error(`Failed to read ${file}: ${err.message}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
const slug = schema.slug;
|
|
296
|
+
const name = schema.catalog_id || schema.slug || file;
|
|
297
|
+
if (!slug) {
|
|
298
|
+
console.error("Schema must have a 'slug' field");
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
const status = opts.publish ? "published" : "draft";
|
|
302
|
+
const spinner = ora3(`Pushing catalog "${slug}"...`).start();
|
|
303
|
+
try {
|
|
304
|
+
const listRes = await api.get("/api/v1/catalogs");
|
|
305
|
+
const catalogs = listRes.data || [];
|
|
306
|
+
const existing = catalogs.find((c) => c.slug === slug);
|
|
307
|
+
if (existing) {
|
|
308
|
+
const res = await api.put(`/api/v1/catalogs/${existing.catalog_id}`, {
|
|
309
|
+
schema,
|
|
310
|
+
status,
|
|
311
|
+
name
|
|
312
|
+
});
|
|
313
|
+
spinner.succeed(`Updated catalog "${slug}" (${existing.catalog_id})`);
|
|
314
|
+
if (res.data?.url) {
|
|
315
|
+
console.log(` URL: ${res.data.url}`);
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
const res = await api.post("/api/v1/catalogs", {
|
|
319
|
+
slug,
|
|
320
|
+
name,
|
|
321
|
+
schema,
|
|
322
|
+
status
|
|
323
|
+
});
|
|
324
|
+
spinner.succeed(`Created catalog "${slug}" (${res.data?.catalog_id})`);
|
|
325
|
+
if (res.data?.url) {
|
|
326
|
+
console.log(` URL: ${res.data.url}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
console.log(` Status: ${status}
|
|
330
|
+
`);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
spinner.fail(`Push failed: ${err.message}`);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/commands/catalog-list.ts
|
|
338
|
+
import ora4 from "ora";
|
|
339
|
+
async function catalogList() {
|
|
340
|
+
const config = requireConfig();
|
|
341
|
+
const api = new ApiClient(config);
|
|
342
|
+
const spinner = ora4("Fetching catalogs...").start();
|
|
343
|
+
try {
|
|
344
|
+
const res = await api.get("/api/v1/catalogs");
|
|
345
|
+
const catalogs = res.data || [];
|
|
346
|
+
spinner.stop();
|
|
347
|
+
if (catalogs.length === 0) {
|
|
348
|
+
console.log("\nNo catalogs found.\n");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
console.log(`
|
|
352
|
+
Found ${catalogs.length} catalog(s):
|
|
353
|
+
`);
|
|
354
|
+
for (const c of catalogs) {
|
|
355
|
+
const statusBadge = c.status === "published" ? "[published]" : "[draft]";
|
|
356
|
+
console.log(` ${c.slug} ${statusBadge}`);
|
|
357
|
+
console.log(` id: ${c.catalog_id}`);
|
|
358
|
+
if (c.url) console.log(` url: ${c.url}`);
|
|
359
|
+
console.log();
|
|
360
|
+
}
|
|
361
|
+
} catch (err) {
|
|
362
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/commands/whoami.ts
|
|
368
|
+
async function whoami() {
|
|
369
|
+
const config = getConfig();
|
|
370
|
+
if (!config.api_url || !config.token) {
|
|
371
|
+
console.log("\nNot configured.\n");
|
|
372
|
+
console.log("Set these environment variables:");
|
|
373
|
+
console.log(" CATALOGS_API_URL \u2014 API base URL (e.g. https://catalog-funnel-api-staging.cloud.zoomgtm.com)");
|
|
374
|
+
console.log(" CATALOGS_TOKEN \u2014 API key (cfk_... or base64 legacy token)");
|
|
375
|
+
console.log("\nOr create ~/.catalogs-cli/config.json:");
|
|
376
|
+
console.log(' { "api_url": "...", "token": "..." }\n');
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const tokenPreview = config.token.length > 12 ? config.token.slice(0, 8) + "..." + config.token.slice(-4) : "***";
|
|
380
|
+
console.log(`
|
|
381
|
+
api_url: ${config.api_url}`);
|
|
382
|
+
console.log(` token: ${tokenPreview}`);
|
|
383
|
+
try {
|
|
384
|
+
const res = await fetch(`${config.api_url}/health`);
|
|
385
|
+
if (res.ok) {
|
|
386
|
+
const data = await res.json();
|
|
387
|
+
console.log(` stage: ${data.stage || "unknown"}`);
|
|
388
|
+
console.log(` status: connected`);
|
|
389
|
+
} else {
|
|
390
|
+
console.log(` status: error (${res.status})`);
|
|
391
|
+
}
|
|
392
|
+
} catch (err) {
|
|
393
|
+
console.log(` status: unreachable (${err.message})`);
|
|
394
|
+
}
|
|
395
|
+
console.log();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/index.ts
|
|
399
|
+
var program = new Command();
|
|
400
|
+
program.name("catalogs").description("CLI for Catalog Funnel \u2014 upload videos, push catalogs, manage assets").version("0.1.0");
|
|
401
|
+
var video = program.command("video").description("Video upload & transcoding");
|
|
402
|
+
video.command("upload <file>").description("Upload a video file \u2192 transcode to HLS \u2192 return hls_url").option("--no-transcode", "Skip transcoding (upload only)").action(videoUpload);
|
|
403
|
+
video.command("status <videoId>").description("Check transcode status for a video").action(videoStatus);
|
|
404
|
+
var catalog = program.command("catalog").description("Catalog schema management");
|
|
405
|
+
catalog.command("push <file>").description("Create or update a catalog from a JSON schema file").option("--publish", "Set status to published (default: draft)").action(catalogPush);
|
|
406
|
+
catalog.command("list").description("List all catalogs").action(catalogList);
|
|
407
|
+
program.command("whoami").description("Show current authentication info").action(whoami);
|
|
408
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@officexapp/catalogs-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for Catalog Funnel — upload videos, push catalogs, manage assets",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"catalogs": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup",
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"catalog-funnel",
|
|
19
|
+
"officex",
|
|
20
|
+
"cli",
|
|
21
|
+
"video",
|
|
22
|
+
"hls"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/OfficeXApp/catalogs-cli.git"
|
|
27
|
+
},
|
|
28
|
+
"license": "UNLICENSED",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"commander": "^12.1.0",
|
|
34
|
+
"ora": "^8.1.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"tsup": "^8.3.0",
|
|
39
|
+
"tsx": "^4.19.0",
|
|
40
|
+
"typescript": "^5.6.3"
|
|
41
|
+
}
|
|
42
|
+
}
|