@officexapp/catalogs-cli 0.1.0 → 0.2.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 +9 -5
- package/dist/index.js +158 -77
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @officexapp/catalogs-cli
|
|
2
2
|
|
|
3
|
-
CLI for Catalog
|
|
3
|
+
CLI for Catalog Kit — upload videos, push catalog schemas, manage assets.
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
@@ -11,15 +11,19 @@ npm install -g @officexapp/catalogs-cli
|
|
|
11
11
|
Configure authentication:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
export
|
|
15
|
-
export CATALOGS_TOKEN="cfk_your_api_key_here"
|
|
14
|
+
export CATALOG_KIT_TOKEN="cfk_your_api_key_here"
|
|
16
15
|
```
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
The API URL defaults to `https://api.catalogkit.cc`. To override (e.g. for staging):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
export CATALOG_KIT_API_URL="https://api-staging.catalogkit.cc"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or create `~/.catalog-kit/config.json`:
|
|
19
24
|
|
|
20
25
|
```json
|
|
21
26
|
{
|
|
22
|
-
"api_url": "https://catalog-funnel-api-staging.cloud.zoomgtm.com",
|
|
23
27
|
"token": "cfk_your_api_key_here"
|
|
24
28
|
}
|
|
25
29
|
```
|
package/dist/index.js
CHANGED
|
@@ -3,71 +3,33 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
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
6
|
// src/config.ts
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
var DEFAULT_API_URL = "https://api.catalogkit.cc";
|
|
8
|
+
var globalTokenOverride;
|
|
9
|
+
function setGlobalToken(token) {
|
|
10
|
+
globalTokenOverride = token;
|
|
11
|
+
}
|
|
17
12
|
function getConfig() {
|
|
18
|
-
const envUrl = process.env.
|
|
19
|
-
const
|
|
20
|
-
|
|
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
|
-
};
|
|
13
|
+
const envUrl = process.env.CATALOG_KIT_API_URL;
|
|
14
|
+
const token = globalTokenOverride || process.env.CATALOG_KIT_TOKEN || "";
|
|
15
|
+
return { api_url: envUrl || DEFAULT_API_URL, token };
|
|
53
16
|
}
|
|
54
17
|
function requireConfig() {
|
|
55
18
|
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
19
|
if (!config.token) {
|
|
63
20
|
console.error(
|
|
64
|
-
|
|
21
|
+
'\nMissing auth token.\n\n Set the CATALOG_KIT_TOKEN environment variable:\n export CATALOG_KIT_TOKEN="cfk_..."\n\n Or pass --token on the command line:\n catalogs --token cfk_... catalog push schema.json\n'
|
|
65
22
|
);
|
|
66
23
|
process.exit(1);
|
|
67
24
|
}
|
|
68
25
|
return config;
|
|
69
26
|
}
|
|
70
27
|
|
|
28
|
+
// src/commands/video-upload.ts
|
|
29
|
+
import { readFileSync, statSync } from "fs";
|
|
30
|
+
import { basename } from "path";
|
|
31
|
+
import ora from "ora";
|
|
32
|
+
|
|
71
33
|
// src/api.ts
|
|
72
34
|
var ApiClient = class {
|
|
73
35
|
constructor(config) {
|
|
@@ -116,6 +78,26 @@ var ApiClient = class {
|
|
|
116
78
|
}
|
|
117
79
|
};
|
|
118
80
|
|
|
81
|
+
// src/lib/identity.ts
|
|
82
|
+
async function printIdentity(api) {
|
|
83
|
+
try {
|
|
84
|
+
const res = await api.get("/api/v1/me");
|
|
85
|
+
if (res.ok && res.data) {
|
|
86
|
+
const d = res.data;
|
|
87
|
+
const parts = [`Authenticated as ${d.subdomain || d.user_id}`];
|
|
88
|
+
if (d.email) parts.push(`(${d.email})`);
|
|
89
|
+
console.log(`
|
|
90
|
+
${parts.join(" ")}`);
|
|
91
|
+
if (d.subdomain) {
|
|
92
|
+
console.log(` Subdomain: ${d.subdomain}.catalogkit.cc`);
|
|
93
|
+
}
|
|
94
|
+
console.log();
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
console.warn("\n Warning: Could not verify identity (API unreachable)\n");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
119
101
|
// src/commands/video-upload.ts
|
|
120
102
|
var MIME_MAP = {
|
|
121
103
|
".mp4": "video/mp4",
|
|
@@ -140,6 +122,7 @@ function formatBytes(bytes) {
|
|
|
140
122
|
async function videoUpload(file, opts) {
|
|
141
123
|
const config = requireConfig();
|
|
142
124
|
const api = new ApiClient(config);
|
|
125
|
+
await printIdentity(api);
|
|
143
126
|
const skipTranscode = opts.transcode === false;
|
|
144
127
|
let stat;
|
|
145
128
|
try {
|
|
@@ -175,7 +158,7 @@ File: ${filename} (${formatBytes(sizeBytes)})`);
|
|
|
175
158
|
}
|
|
176
159
|
const uploadSpinner = ora("Uploading to S3...").start();
|
|
177
160
|
try {
|
|
178
|
-
const fileBuffer =
|
|
161
|
+
const fileBuffer = readFileSync(file);
|
|
179
162
|
const res = await fetch(uploadUrl, {
|
|
180
163
|
method: "PUT",
|
|
181
164
|
headers: { "Content-Type": contentType },
|
|
@@ -247,7 +230,7 @@ Check status later: catalogs video status ${videoId}
|
|
|
247
230
|
`);
|
|
248
231
|
}
|
|
249
232
|
function sleep(ms) {
|
|
250
|
-
return new Promise((
|
|
233
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
251
234
|
}
|
|
252
235
|
|
|
253
236
|
// src/commands/video-status.ts
|
|
@@ -279,25 +262,93 @@ async function videoStatus(videoId) {
|
|
|
279
262
|
}
|
|
280
263
|
|
|
281
264
|
// src/commands/catalog-push.ts
|
|
282
|
-
import { readFileSync as
|
|
265
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
266
|
+
import { resolve, extname } from "path";
|
|
267
|
+
import { pathToFileURL } from "url";
|
|
283
268
|
import ora3 from "ora";
|
|
269
|
+
|
|
270
|
+
// src/lib/serialize.ts
|
|
271
|
+
var MAX_CATALOG_SIZE_KB = 512;
|
|
272
|
+
function serializeCatalog(obj) {
|
|
273
|
+
if (obj === null || obj === void 0) return obj;
|
|
274
|
+
if (typeof obj === "function") {
|
|
275
|
+
return obj.toString();
|
|
276
|
+
}
|
|
277
|
+
if (Array.isArray(obj)) {
|
|
278
|
+
return obj.map((item) => serializeCatalog(item));
|
|
279
|
+
}
|
|
280
|
+
if (typeof obj === "object") {
|
|
281
|
+
const result = {};
|
|
282
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
283
|
+
result[key] = serializeCatalog(value);
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
return obj;
|
|
288
|
+
}
|
|
289
|
+
function validateCatalog(schema) {
|
|
290
|
+
const errors = [];
|
|
291
|
+
if (!schema.slug) {
|
|
292
|
+
errors.push("Schema must have a 'slug' field");
|
|
293
|
+
}
|
|
294
|
+
if (!schema.schema_version) {
|
|
295
|
+
errors.push("Schema must have a 'schema_version' field");
|
|
296
|
+
}
|
|
297
|
+
if (!schema.settings?.theme) {
|
|
298
|
+
errors.push("Schema must have 'settings.theme'");
|
|
299
|
+
}
|
|
300
|
+
if (!schema.pages || Object.keys(schema.pages).length === 0) {
|
|
301
|
+
errors.push("Schema must have at least one page in 'pages'");
|
|
302
|
+
}
|
|
303
|
+
if (!schema.routing) {
|
|
304
|
+
errors.push("Schema must have a 'routing' object");
|
|
305
|
+
}
|
|
306
|
+
const json = JSON.stringify(schema);
|
|
307
|
+
const sizeKB = Math.round(json.length / 1024);
|
|
308
|
+
if (sizeKB > MAX_CATALOG_SIZE_KB) {
|
|
309
|
+
errors.push(
|
|
310
|
+
`Catalog too large (${sizeKB}KB). Max: ${MAX_CATALOG_SIZE_KB}KB. Move heavy assets to settings.scripts[] CDN imports.`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
return errors;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/commands/catalog-push.ts
|
|
317
|
+
async function loadTsFile(file) {
|
|
318
|
+
const abs = resolve(file);
|
|
319
|
+
const { register } = await import("module");
|
|
320
|
+
register("tsx/esm", pathToFileURL("./"));
|
|
321
|
+
const mod = await import(pathToFileURL(abs).href);
|
|
322
|
+
return mod.default ?? mod;
|
|
323
|
+
}
|
|
284
324
|
async function catalogPush(file, opts) {
|
|
285
325
|
const config = requireConfig();
|
|
286
326
|
const api = new ApiClient(config);
|
|
327
|
+
await printIdentity(api);
|
|
328
|
+
const ext = extname(file).toLowerCase();
|
|
329
|
+
const isTs = ext === ".ts" || ext === ".mts";
|
|
287
330
|
let schema;
|
|
288
331
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
332
|
+
if (isTs) {
|
|
333
|
+
const rawCatalog = await loadTsFile(file);
|
|
334
|
+
schema = serializeCatalog(rawCatalog);
|
|
335
|
+
} else {
|
|
336
|
+
const raw = readFileSync2(file, "utf-8");
|
|
337
|
+
schema = JSON.parse(raw);
|
|
338
|
+
}
|
|
291
339
|
} catch (err) {
|
|
292
340
|
console.error(`Failed to read ${file}: ${err.message}`);
|
|
293
341
|
process.exit(1);
|
|
294
342
|
}
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
343
|
+
const errors = validateCatalog(schema);
|
|
344
|
+
if (errors.length > 0) {
|
|
345
|
+
for (const err of errors) {
|
|
346
|
+
console.error(`Error: ${err}`);
|
|
347
|
+
}
|
|
299
348
|
process.exit(1);
|
|
300
349
|
}
|
|
350
|
+
const slug = schema.slug;
|
|
351
|
+
const name = schema.catalog_id || schema.slug || file;
|
|
301
352
|
const status = opts.publish ? "published" : "draft";
|
|
302
353
|
const spinner = ora3(`Pushing catalog "${slug}"...`).start();
|
|
303
354
|
try {
|
|
@@ -367,13 +418,14 @@ async function catalogList() {
|
|
|
367
418
|
// src/commands/whoami.ts
|
|
368
419
|
async function whoami() {
|
|
369
420
|
const config = getConfig();
|
|
370
|
-
if (!config.
|
|
421
|
+
if (!config.token) {
|
|
371
422
|
console.log("\nNot configured.\n");
|
|
372
|
-
console.log("Set
|
|
373
|
-
console.log("
|
|
374
|
-
console.log("
|
|
375
|
-
console.log("\
|
|
376
|
-
console.log(
|
|
423
|
+
console.log("Set this environment variable:");
|
|
424
|
+
console.log(" CATALOG_KIT_TOKEN \u2014 API key (cfk_... or base64 legacy token)");
|
|
425
|
+
console.log("\nOptionally override the API URL (defaults to https://api.catalogkit.cc):");
|
|
426
|
+
console.log(" CATALOG_KIT_API_URL \u2014 API base URL");
|
|
427
|
+
console.log("\nOr create ~/.catalog-kit/config.json:");
|
|
428
|
+
console.log(' { "token": "cfk_..." }\n');
|
|
377
429
|
return;
|
|
378
430
|
}
|
|
379
431
|
const tokenPreview = config.token.length > 12 ? config.token.slice(0, 8) + "..." + config.token.slice(-4) : "***";
|
|
@@ -381,28 +433,57 @@ async function whoami() {
|
|
|
381
433
|
api_url: ${config.api_url}`);
|
|
382
434
|
console.log(` token: ${tokenPreview}`);
|
|
383
435
|
try {
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
console.log(` status:
|
|
389
|
-
|
|
390
|
-
console.log(`
|
|
436
|
+
const api = new ApiClient(config);
|
|
437
|
+
const res = await api.get("/api/v1/me");
|
|
438
|
+
if (res.ok && res.data) {
|
|
439
|
+
const d = res.data;
|
|
440
|
+
console.log(` status: authenticated`);
|
|
441
|
+
console.log();
|
|
442
|
+
console.log(` user_id: ${d.user_id}`);
|
|
443
|
+
if (d.email) console.log(` email: ${d.email}`);
|
|
444
|
+
if (d.username) console.log(` username: ${d.username}`);
|
|
445
|
+
if (d.subdomain) console.log(` subdomain: ${d.subdomain}`);
|
|
446
|
+
if (d.custom_domain) console.log(` custom_domain: ${d.custom_domain}`);
|
|
447
|
+
if (d.catalog_url) console.log(` catalog_url: ${d.catalog_url}`);
|
|
448
|
+
console.log(` credits: ${d.credits}`);
|
|
449
|
+
console.log(` auth_method: ${d.auth_method}`);
|
|
450
|
+
if (d.api_key_id) console.log(` api_key_id: ${d.api_key_id}`);
|
|
451
|
+
console.log(` is_superadmin: ${d.is_superadmin}`);
|
|
452
|
+
console.log(` stripe_key_set: ${d.stripe_key_set}`);
|
|
453
|
+
if (d.stripe_key_last4) console.log(` stripe_key_last4: ...${d.stripe_key_last4}`);
|
|
454
|
+
console.log(` officex: ${d.officex_connected ? "connected" : "not connected"}`);
|
|
455
|
+
console.log(` created_at: ${d.created_at}`);
|
|
391
456
|
}
|
|
392
457
|
} catch (err) {
|
|
393
|
-
|
|
458
|
+
try {
|
|
459
|
+
const res = await fetch(`${config.api_url}/health`);
|
|
460
|
+
if (res.ok) {
|
|
461
|
+
const data = await res.json();
|
|
462
|
+
console.log(` stage: ${data.stage || "unknown"}`);
|
|
463
|
+
console.log(` status: connected (could not fetch user: ${err.message})`);
|
|
464
|
+
} else {
|
|
465
|
+
console.log(` status: error (${res.status})`);
|
|
466
|
+
}
|
|
467
|
+
} catch (healthErr) {
|
|
468
|
+
console.log(` status: unreachable (${healthErr.message})`);
|
|
469
|
+
}
|
|
394
470
|
}
|
|
395
471
|
console.log();
|
|
396
472
|
}
|
|
397
473
|
|
|
398
474
|
// src/index.ts
|
|
399
475
|
var program = new Command();
|
|
400
|
-
program.name("catalogs").description("CLI for Catalog
|
|
476
|
+
program.name("catalogs").description("CLI for Catalog Kit \u2014 upload videos, push catalogs, manage assets").version("0.1.0").option("--token <token>", "Auth token (overrides CATALOG_KIT_TOKEN env var)").hook("preAction", (thisCommand) => {
|
|
477
|
+
const opts = thisCommand.opts();
|
|
478
|
+
if (opts.token) {
|
|
479
|
+
setGlobalToken(opts.token);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
401
482
|
var video = program.command("video").description("Video upload & transcoding");
|
|
402
483
|
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
484
|
video.command("status <videoId>").description("Check transcode status for a video").action(videoStatus);
|
|
404
485
|
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);
|
|
486
|
+
catalog.command("push <file>").description("Create or update a catalog from a JSON or TypeScript schema file").option("--publish", "Set status to published (default: draft)").action(catalogPush);
|
|
406
487
|
catalog.command("list").description("List all catalogs").action(catalogList);
|
|
407
488
|
program.command("whoami").description("Show current authentication info").action(whoami);
|
|
408
489
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@officexapp/catalogs-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI for Catalog
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "CLI for Catalog Kit — upload videos, push catalogs, manage assets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"catalogs": "./dist/index.js"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"dist"
|
|
16
16
|
],
|
|
17
17
|
"keywords": [
|
|
18
|
-
"catalog-
|
|
18
|
+
"catalog-kit",
|
|
19
19
|
"officex",
|
|
20
20
|
"cli",
|
|
21
21
|
"video",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"commander": "^12.1.0",
|
|
34
|
-
"ora": "^8.1.0"
|
|
34
|
+
"ora": "^8.1.0",
|
|
35
|
+
"tsx": "^4.19.0"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@types/node": "^22.0.0",
|
|
38
39
|
"tsup": "^8.3.0",
|
|
39
|
-
"tsx": "^4.19.0",
|
|
40
40
|
"typescript": "^5.6.3"
|
|
41
41
|
}
|
|
42
42
|
}
|