faceless-cli 1.1.7 → 1.1.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "faceless-cli",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "CLI for Faceless.so: create AI faceless videos, run automated series and publish to YouTube, TikTok, Instagram and more.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/client.mjs CHANGED
@@ -1,129 +1,122 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
 
3
3
  export class CliError extends Error {
4
- constructor(type, message, status) {
5
- super(message);
6
- this.name = "CliError";
7
- this.type = type || "internal_error";
8
- this.status = status;
9
- }
4
+ constructor(type, message, status) {
5
+ super(message);
6
+ this.name = "CliError";
7
+ this.type = type || "internal_error";
8
+ this.status = status;
9
+ }
10
10
  }
11
11
 
12
12
  export function exitCodeFor(err) {
13
- const type = err && err.type;
14
- if (type === "unauthorized" || type === "forbidden_scope") return 2;
15
- if (type === "insufficient_credits") return 3;
16
- if (type === "rate_limited") return 4;
17
- return 1;
13
+ const type = err && err.type;
14
+ if (type === "unauthorized" || type === "forbidden_scope") return 2;
15
+ if (type === "insufficient_credits") return 3;
16
+ if (type === "rate_limited") return 4;
17
+ return 1;
18
18
  }
19
19
 
20
20
  function typeForStatus(status) {
21
- switch (status) {
22
- case 400:
23
- return "invalid_input";
24
- case 401:
25
- return "unauthorized";
26
- case 402:
27
- return "insufficient_credits";
28
- case 403:
29
- return "forbidden_scope";
30
- case 404:
31
- return "not_found";
32
- case 409:
33
- return "conflict";
34
- case 429:
35
- return "rate_limited";
36
- default:
37
- return "internal_error";
38
- }
21
+ switch (status) {
22
+ case 400:
23
+ return "invalid_input";
24
+ case 401:
25
+ return "unauthorized";
26
+ case 402:
27
+ return "insufficient_credits";
28
+ case 403:
29
+ return "forbidden_scope";
30
+ case 404:
31
+ return "not_found";
32
+ case 409:
33
+ return "conflict";
34
+ case 429:
35
+ return "rate_limited";
36
+ default:
37
+ return "internal_error";
38
+ }
39
39
  }
40
40
 
41
41
  function buildUrl(baseUrl, path, query) {
42
- const url = new URL(baseUrl + path);
43
- if (query) {
44
- for (const [key, value] of Object.entries(query)) {
45
- if (value === undefined || value === null || value === "") continue;
46
- url.searchParams.set(key, String(value));
47
- }
48
- }
49
- return url;
42
+ const url = new URL(baseUrl + path);
43
+ if (query) {
44
+ for (const [key, value] of Object.entries(query)) {
45
+ if (value === undefined || value === null || value === "") continue;
46
+ url.searchParams.set(key, String(value));
47
+ }
48
+ }
49
+ return url;
50
50
  }
51
51
 
52
52
  export function sleep(ms) {
53
- return new Promise((resolve) => setTimeout(resolve, ms));
53
+ return new Promise((resolve) => setTimeout(resolve, ms));
54
54
  }
55
55
 
56
56
  function retryAfterMs(res) {
57
- const header = res.headers.get("retry-after");
58
- let seconds = Number(header);
59
- if (!Number.isFinite(seconds) || seconds < 0) seconds = 1;
60
- return Math.min(seconds, 30) * 1000;
57
+ const header = res.headers.get("retry-after");
58
+ let seconds = Number(header);
59
+ if (!Number.isFinite(seconds) || seconds < 0) seconds = 1;
60
+ return Math.min(seconds, 30) * 1000;
61
61
  }
62
62
 
63
63
  export async function request({ method, path, query, body, apiKey, baseUrl, idempotencyKey }) {
64
- if (!apiKey) {
65
- throw new CliError(
66
- "unauthorized",
67
- 'No API key configured. Run "faceless login" or set FACELESS_API_KEY.'
68
- );
69
- }
64
+ // Credential order: an explicit key (flag > env > saved key, resolved by the caller) wins; with
65
+ // no key, fall back to the stored OAuth session from `faceless login`, refreshing it as needed.
66
+ // The import is lazy because oauth.mjs imports CliError from this file.
67
+ let bearer = apiKey;
68
+ if (!bearer) {
69
+ const { ensureOauthAccessToken } = await import("./oauth.mjs");
70
+ bearer = await ensureOauthAccessToken();
71
+ }
72
+ if (!bearer) {
73
+ throw new CliError("unauthorized", 'Not authenticated. Run "faceless login" (opens your browser, no key needed) or set FACELESS_API_KEY.');
74
+ }
70
75
 
71
- const url = buildUrl(baseUrl, path, query);
72
- const headers = { Authorization: `Bearer ${apiKey}` };
73
- const httpMethod = method.toUpperCase();
74
- const init = { method: httpMethod, headers };
75
- if (body !== undefined && body !== null) {
76
- headers["Content-Type"] = "application/json";
77
- init.body = JSON.stringify(body);
78
- }
79
- // Every mutating request carries an Idempotency-Key so a retry can never be
80
- // applied (or charge credits) twice. Ops without server-side idempotency
81
- // simply ignore the header. A user-provided --idempotency-key wins.
82
- const isMutating = httpMethod !== "GET" && httpMethod !== "HEAD";
83
- if (isMutating && !idempotencyKey) idempotencyKey = randomUUID();
84
- if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
76
+ const url = buildUrl(baseUrl, path, query);
77
+ const headers = { Authorization: `Bearer ${bearer}` };
78
+ const httpMethod = method.toUpperCase();
79
+ const init = { method: httpMethod, headers };
80
+ if (body !== undefined && body !== null) {
81
+ headers["Content-Type"] = "application/json";
82
+ init.body = JSON.stringify(body);
83
+ }
84
+ // Every mutating request carries an Idempotency-Key so a retry can never be
85
+ // applied (or charge credits) twice. Ops without server-side idempotency
86
+ // simply ignore the header. A user-provided --idempotency-key wins.
87
+ const isMutating = httpMethod !== "GET" && httpMethod !== "HEAD";
88
+ if (isMutating && !idempotencyKey) idempotencyKey = randomUUID();
89
+ if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
85
90
 
86
- let res;
87
- try {
88
- res = await fetch(url, init);
89
- // Retry a rate-limited request once. Insufficient credits is 402 (never
90
- // retried); mutating retries are safe because of the Idempotency-Key.
91
- if (res.status === 429) {
92
- await sleep(retryAfterMs(res));
93
- res = await fetch(url, init);
94
- }
95
- } catch (err) {
96
- throw new CliError("internal_error", `Network error calling ${url.origin}: ${err.message}`);
97
- }
91
+ let res;
92
+ try {
93
+ res = await fetch(url, init);
94
+ // Retry a rate-limited request once. Insufficient credits is 402 (never
95
+ // retried); mutating retries are safe because of the Idempotency-Key.
96
+ if (res.status === 429) {
97
+ await sleep(retryAfterMs(res));
98
+ res = await fetch(url, init);
99
+ }
100
+ } catch (err) {
101
+ throw new CliError("internal_error", `Network error calling ${url.origin}: ${err.message}`);
102
+ }
98
103
 
99
- const text = await res.text();
100
- let parsed = null;
101
- if (text) {
102
- try {
103
- parsed = JSON.parse(text);
104
- } catch {
105
- throw new CliError(
106
- typeForStatus(res.status),
107
- `Unexpected non-JSON response (HTTP ${res.status})`,
108
- res.status
109
- );
110
- }
111
- }
104
+ const text = await res.text();
105
+ let parsed = null;
106
+ if (text) {
107
+ try {
108
+ parsed = JSON.parse(text);
109
+ } catch {
110
+ throw new CliError(typeForStatus(res.status), `Unexpected non-JSON response (HTTP ${res.status})`, res.status);
111
+ }
112
+ }
112
113
 
113
- if (parsed && parsed.success === false) {
114
- const e = parsed.error || {};
115
- throw new CliError(
116
- e.type || typeForStatus(res.status),
117
- e.message || `Request failed (HTTP ${res.status})`,
118
- res.status
119
- );
120
- }
121
- if (!res.ok) {
122
- throw new CliError(
123
- typeForStatus(res.status),
124
- `Request failed (HTTP ${res.status})`,
125
- res.status
126
- );
127
- }
128
- return parsed;
114
+ if (parsed && parsed.success === false) {
115
+ const e = parsed.error || {};
116
+ throw new CliError(e.type || typeForStatus(res.status), e.message || `Request failed (HTTP ${res.status})`, res.status);
117
+ }
118
+ if (!res.ok) {
119
+ throw new CliError(typeForStatus(res.status), `Request failed (HTTP ${res.status})`, res.status);
120
+ }
121
+ return parsed;
129
122
  }
package/src/config.mjs CHANGED
@@ -3,45 +3,45 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
 
5
5
  export const DEFAULT_BASE_URL = "https://faceless.so/api/v1";
6
- export const CONFIG_DIR = path.join(os.homedir(), ".faceless");
6
+ // FACELESS_CONFIG_DIR exists for tests and for containers that mount config somewhere writable.
7
+ export const CONFIG_DIR = process.env.FACELESS_CONFIG_DIR || path.join(os.homedir(), ".faceless");
7
8
  export const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
8
9
 
9
10
  export function loadConfig() {
10
- try {
11
- const raw = fs.readFileSync(CONFIG_PATH, "utf8");
12
- const parsed = JSON.parse(raw);
13
- return parsed && typeof parsed === "object" ? parsed : {};
14
- } catch {
15
- return {};
16
- }
11
+ try {
12
+ const raw = fs.readFileSync(CONFIG_PATH, "utf8");
13
+ const parsed = JSON.parse(raw);
14
+ return parsed && typeof parsed === "object" ? parsed : {};
15
+ } catch {
16
+ return {};
17
+ }
17
18
  }
18
19
 
19
20
  export function saveConfig(partial) {
20
- const config = { ...loadConfig(), ...partial };
21
- for (const key of Object.keys(config)) {
22
- if (config[key] === undefined || config[key] === null) delete config[key];
23
- }
24
- fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
25
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
26
- mode: 0o600,
27
- });
28
- try {
29
- fs.chmodSync(CONFIG_PATH, 0o600);
30
- } catch {
31
- /* best effort on platforms without chmod */
32
- }
33
- return config;
21
+ const config = { ...loadConfig(), ...partial };
22
+ for (const key of Object.keys(config)) {
23
+ if (config[key] === undefined || config[key] === null) delete config[key];
24
+ }
25
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
26
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
27
+ mode: 0o600,
28
+ });
29
+ try {
30
+ fs.chmodSync(CONFIG_PATH, 0o600);
31
+ } catch {
32
+ /* best effort on platforms without chmod */
33
+ }
34
+ return config;
34
35
  }
35
36
 
36
37
  export function resolveApiKey(flags = {}) {
37
- if (flags.apiKey) return flags.apiKey;
38
- if (process.env.FACELESS_API_KEY) return process.env.FACELESS_API_KEY;
39
- const config = loadConfig();
40
- return config.apiKey || null;
38
+ if (flags.apiKey) return flags.apiKey;
39
+ if (process.env.FACELESS_API_KEY) return process.env.FACELESS_API_KEY;
40
+ const config = loadConfig();
41
+ return config.apiKey || null;
41
42
  }
42
43
 
43
44
  export function resolveBaseUrl(flags = {}) {
44
- const url =
45
- flags.apiUrl || process.env.FACELESS_API_URL || loadConfig().baseUrl || DEFAULT_BASE_URL;
46
- return String(url).replace(/\/+$/, "");
45
+ const url = flags.apiUrl || process.env.FACELESS_API_URL || loadConfig().baseUrl || DEFAULT_BASE_URL;
46
+ return String(url).replace(/\/+$/, "");
47
47
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "generatedFrom": "src/backend/api/v1/spec/registry.mjs",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "baseUrl": "https://faceless.so/api/v1",
5
5
  "operations": [
6
6
  {