@facelessad/cli 1.0.0 → 1.0.1

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.
Files changed (3) hide show
  1. package/README.md +29 -5
  2. package/index.js +172 -6
  3. package/package.json +18 -5
package/README.md CHANGED
@@ -9,8 +9,8 @@ facelessad tools # what can be made
9
9
  facelessad create --tool motion-graphics --url https://your-product.com --wait
10
10
  ```
11
11
 
12
- Nine commands: `login`, `create`, `status`, `list`, `download`, `estimate`,
13
- `tools`, `voices`, `balance`. Every command accepts `--json` for scripting;
12
+ Ten commands: `login`, `create`, `status`, `list`, `download`, `estimate`,
13
+ `tools`, `voices`, `brands`, `balance`. Every command accepts `--json` for scripting;
14
14
  `create`/`estimate` accept `--dry-run` (print the request body without
15
15
  sending) and `--file body.json` (a base body that flags override).
16
16
 
@@ -18,9 +18,33 @@ The key is stored in `~/.facelessad/config.json` with mode 0600. The
18
18
  environment variable `FACELESSAD_API_KEY` always wins — that is the way CI
19
19
  should authenticate. `FACELESSAD_API_URL` overrides the API base.
20
20
 
21
- `--wait` polls and prints phase + elapsed time; for long-running builds a
22
- webhook is the better tool a stuck shell is a stuck CI job. See
23
- https://facelessad.com/developers Webhooks.
21
+ `--wait` polls and prints phase + elapsed time, and gives up after 30
22
+ minutes (`--timeout <seconds>` changes that). A video whose status is
23
+ `draft` is not building and will never finish on its own, so `--wait`
24
+ stops on it immediately instead of polling forever. For long-running
25
+ builds a webhook is still the better tool — a stuck shell is a stuck CI
26
+ job. See https://facelessad.com/developers → Webhooks.
27
+
28
+ ## Flag spellings (1.0.1)
29
+
30
+ Both spellings are accepted, because the older ones appeared on the
31
+ developers page and live on in scripts:
32
+
33
+ | also accepted | canonical |
34
+ |---|---|
35
+ | `--brand` | `--brand-name` |
36
+ | `--color`, `--colour` | `--brand-color` |
37
+ | `--voice-id` | `--voice` |
38
+ | `--voice-gender` | `--gender` |
39
+ | `--no-voice-over` | `--no-voice` |
40
+ | `--no-captions` | `--captions false` |
41
+ | `--no-music` | `--music false` |
42
+ | `-o` | `--out` |
43
+
44
+ `--no-brand-kit` and `--no-winners` now work; before 1.0.1 they were
45
+ accepted on the command line and silently dropped, so the video was built
46
+ with the Brand Kit anyway. **An unknown flag is now an error** rather than
47
+ something quietly ignored — that silence is what hid the whole problem.
24
48
 
25
49
  Errors are printed with the API's own wording and exit code 1. The machine
26
50
  code (e.g. `unknown_style`) follows the message.
package/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * @facelessad/cli — FacelessAd from the terminal.
4
4
  *
5
- * Nine commands, each one /api/v1 call. Zero dependencies: Node 18 ships
5
+ * Ten commands, each one /api/v1 call. Zero dependencies: Node 18 ships
6
6
  * fetch, and a tool that stores an API key is a bad place for a dependency
7
7
  * tree.
8
8
  *
@@ -30,17 +30,43 @@ const bold = (s) => (isTTY ? `\x1b[1m${s}\x1b[0m` : s);
30
30
 
31
31
  // ────────────────────────── arg parsing ──────────────────────────
32
32
 
33
+ /**
34
+ * Onko token lippu eikä arvo? Pitkä `--x` tai yhden kirjaimen lyhyt `-o`.
35
+ *
36
+ * 1.0.1 KORJAUS: tämä oli aiemmin pelkkä `startsWith('--')`, jolloin
37
+ * lyhytlippu meni edellisen lipun ARVOKSI. `create --dry-run -o x.json`
38
+ * asetti `flags['dry-run'] = '-o'`, ja koska tarkistus on `=== true`,
39
+ * kuivaharjoitus ei laukennut vaan komento loi oikean videon ja veloitti
40
+ * creditit. Lievempi mutta yleisempi osuma oli
41
+ * `download 5 --json -o out.mp4`, jossa molemmat liput katosivat hiljaa.
42
+ *
43
+ * Yhden kirjaimen rajaus on tarkoituksellinen: `--offset -5` toimii yhä,
44
+ * koska `-5` ei ole lippu, ja `--text "-leading dash"` samoin.
45
+ */
46
+ const looksLikeFlag = (t) => t.startsWith('--') || /^-[A-Za-z]$/.test(t);
47
+
33
48
  const rawArgs = process.argv.slice(2);
34
49
  const cmd = rawArgs[0] && !rawArgs[0].startsWith('-') ? rawArgs[0] : (rawArgs.includes('--version') || rawArgs.includes('-v') ? 'version' : 'help');
35
50
  const flags = {};
36
51
  const positional = [];
52
+ const badShort = [];
37
53
  for (let i = cmd === 'help' || cmd === 'version' ? 0 : 1; i < rawArgs.length; i++) {
38
54
  const a = rawArgs[i];
55
+ // 1.0.1: -o <file> on dokumentoitu facelessad.com/developers -sivulla ja
56
+ // päätyi aiemmin positional-listaan, jolloin tiedostonimi jäi huomiotta
57
+ // ja lataus tallentui oletusnimellä. Lyhytliput käsitellään nyt erikseen.
58
+ if (a === '-o') {
59
+ if (i + 1 < rawArgs.length && !looksLikeFlag(rawArgs[i + 1])) flags.out = rawArgs[++i];
60
+ else flags.out = true;
61
+ continue;
62
+ }
63
+ if (a === '-v') { continue; } // versio luetaan jo cmd-tunnistuksessa
64
+ if (/^-[A-Za-z]$/.test(a)) { badShort.push(a); continue; }
39
65
  if (a.startsWith('--')) {
40
66
  const eq = a.indexOf('=');
41
67
  if (eq !== -1) {
42
68
  flags[a.slice(2, eq)] = a.slice(eq + 1);
43
- } else if (i + 1 < rawArgs.length && !rawArgs[i + 1].startsWith('--')) {
69
+ } else if (i + 1 < rawArgs.length && !looksLikeFlag(rawArgs[i + 1])) {
44
70
  flags[a.slice(2)] = rawArgs[++i];
45
71
  } else {
46
72
  flags[a.slice(2)] = true;
@@ -49,6 +75,49 @@ for (let i = cmd === 'help' || cmd === 'version' ? 0 : 1; i < rawArgs.length; i+
49
75
  positional.push(a);
50
76
  }
51
77
  }
78
+
79
+ /**
80
+ * 1.0.1 — ALIAKSET. Tämän paketin lippunimet ajautuivat erilleen siitä mitä
81
+ * facelessad.com/developers tuottaa: sivu kirjoitti --brand, --color,
82
+ * --voice-id, --voice-gender, --no-voice-over, --no-captions ja --no-music,
83
+ * joita tämä tiedosto ei lukenut. Koska tuntematon lippu meni hiljaa
84
+ * roskiin, kopioitu komento ONNISTUI ja teki videon väärillä asetuksilla:
85
+ * ääni päällä vaikka käyttäjä pyysi ilman, brändinimi ja väri kokonaan pois.
86
+ * Sivu korjattiin, mutta vanhat komennot elävät skripteissä ja
87
+ * muistiinpanoissa — siksi molemmat kirjoitusasut hyväksytään täällä.
88
+ */
89
+ const ALIASES = {
90
+ 'brand': 'brand-name',
91
+ 'color': 'brand-color',
92
+ 'colour': 'brand-color',
93
+ 'voice-id': 'voice',
94
+ 'voice-gender': 'gender',
95
+ 'no-voice-over': 'no-voice',
96
+ 'product-image-url': 'product-image',
97
+ };
98
+ for (const [from, to] of Object.entries(ALIASES)) {
99
+ if (flags[from] !== undefined && flags[to] === undefined) flags[to] = flags[from];
100
+ delete flags[from];
101
+ }
102
+ // --no-captions / --no-music ovat kieltomuotoja arvollisista lipuista.
103
+ if (flags['no-captions'] === true) { flags.captions = 'false'; delete flags['no-captions']; }
104
+ if (flags['no-music'] === true) { flags.music = 'false'; delete flags['no-music']; }
105
+
106
+ /**
107
+ * 1.0.1 — TUNTEMATON LIPPU ON VIRHE. Tämä on se ominaisuus jonka puute teki
108
+ * lippujen ajautumisesta näkymätöntä: aiemmin väärin kirjoitettu lippu ei
109
+ * tuottanut mitään palautetta, ja käyttäjä sai videon jota ei tilannut.
110
+ * Nyt komento pysähtyy ennen kuin mitään veloitetaan.
111
+ */
112
+ const KNOWN_FLAGS = new Set([
113
+ 'tool', 'url', 'text', 'duration', 'aspect', 'language', 'style', 'style-hint',
114
+ 'structure', 'hook', 'video-mode', 'brand-color', 'brand-name', 'cta', 'name',
115
+ 'voice', 'gender', 'no-voice', 'music', 'sfx', 'captions', 'product-image',
116
+ 'no-brand-kit', 'no-winners', 'brand-kit',
117
+ 'file', 'dry-run', 'wait', 'timeout', 'json', 'out', 'limit', 'offset', 'version',
118
+ ]);
119
+ const unknownFlags = Object.keys(flags).filter((f) => !KNOWN_FLAGS.has(f)).map((f) => '--' + f).concat(badShort);
120
+
52
121
  const asJson = flags.json === true || flags.json === 'true';
53
122
 
54
123
  // ────────────────────────── config / auth ──────────────────────────
@@ -111,6 +180,22 @@ function out(data, human) {
111
180
  }
112
181
  }
113
182
 
183
+ /**
184
+ * 1.0.1: numeeriset liput. `parseInt('abc')` on NaN, ja JSON.stringify tekee
185
+ * siitä `null` — palvelin ohittaa null-arvon ja käyttää oletusta. Eli
186
+ * `--brand-kit abc` olisi tuottanut hiljaa OLETUSBRÄNDIN ja `--duration abc`
187
+ * oletuskeston, ilman mitään ilmoitusta. Sama hiljainen korvaus jota koko tämä
188
+ * versio on korjaamassa; kelpaamaton luku on virhe.
189
+ */
190
+ function num(flag, raw) {
191
+ if (raw === undefined) return undefined;
192
+ const n = Number(raw);
193
+ if (!Number.isFinite(n) || !Number.isInteger(n)) {
194
+ die('--' + flag + ' must be a whole number, got "' + raw + '"', 'invalid_number');
195
+ }
196
+ return n;
197
+ }
198
+
114
199
  // ────────────────────────── request body from flags ──────────────────────────
115
200
 
116
201
  /**
@@ -130,7 +215,7 @@ function buildBody() {
130
215
  if (flags.url !== undefined) body.materials.landing_page_url = String(flags.url);
131
216
  if (flags.text !== undefined) body.materials.text = String(flags.text);
132
217
  }
133
- set('duration', flags.duration !== undefined ? parseInt(flags.duration, 10) : undefined);
218
+ set('duration', num('duration', flags.duration));
134
219
  set('aspect_ratio', flags.aspect !== undefined ? String(flags.aspect) : undefined);
135
220
  set('language', flags.language !== undefined ? String(flags.language) : undefined);
136
221
  set('style', flags.style !== undefined ? String(flags.style) : undefined);
@@ -143,10 +228,17 @@ function buildBody() {
143
228
  set('cta', flags.cta !== undefined ? String(flags.cta) : undefined);
144
229
  set('name', flags.name !== undefined ? String(flags.name) : undefined);
145
230
  set('product_image_url', flags['product-image'] !== undefined ? String(flags['product-image']) : undefined);
231
+ // 1.0.1 (§640): kumpi brändi. Ilman tätä API otti aina tilin oletusbrändin.
232
+ set('brand_kit_id', num('brand-kit', flags['brand-kit']));
146
233
  if (flags.music !== undefined) body.music = flags.music === true || flags.music === 'true';
147
234
  if (flags.sfx !== undefined) body.sfx = flags.sfx === true || flags.sfx === 'true';
148
235
  if (flags.captions !== undefined) body.captions = flags.captions === true || flags.captions === 'true';
149
236
  if (flags['no-voice'] === true) body.voice_over = false;
237
+ // 1.0.1: nämä kaksi puuttuivat kokonaan. facelessad.com/developers tuotti
238
+ // niille lippuja joita tämä tiedosto ei lukenut, joten "älä käytä brand
239
+ // kittiä" -pyyntö meni hiljaa roskiin ja API:n oletus (true) voitti.
240
+ if (flags['no-brand-kit'] === true) body.use_brand_kit = false;
241
+ if (flags['no-winners'] === true) body.use_winners = false;
150
242
  if (flags.voice !== undefined || flags.gender !== undefined) {
151
243
  if (typeof flags.voice === 'string' && flags.gender === undefined) {
152
244
  body.voice = flags.voice;
@@ -216,6 +308,17 @@ const commands = {
216
308
  out(d, (d.tools || []).map((t) => ' ' + t.id.padEnd(18) + dim(t.name || '')).join('\n') + '\n\nDetails: facelessad tools --tool <id>');
217
309
  },
218
310
 
311
+ async brands() {
312
+ const d = await call('GET', '/api/v1/brand-kits');
313
+ out(d, (d.brandKits || []).map((k) =>
314
+ ' ' + String(k.id).padEnd(6) + (k.name || '').padEnd(24)
315
+ + dim(k.filled + '/32 answered'
316
+ + (k.isDefault ? ' · default' : '')
317
+ + (k.hasColor ? ' · colour' : '')
318
+ + (k.hasLogo ? ' · logo' : ''))
319
+ ).join('\n') || ' (no brands yet)');
320
+ },
321
+
219
322
  async voices() {
220
323
  const q = new URLSearchParams();
221
324
  if (flags.language) q.set('language', String(flags.language));
@@ -253,8 +356,9 @@ const commands = {
253
356
 
254
357
  async list() {
255
358
  const q = new URLSearchParams();
256
- if (flags.limit) q.set('limit', String(flags.limit));
257
- if (flags.offset) q.set('offset', String(flags.offset));
359
+ const lim = num('limit', flags.limit), off = num('offset', flags.offset);
360
+ if (lim !== undefined) q.set('limit', String(lim));
361
+ if (off !== undefined) q.set('offset', String(off));
258
362
  const d = await call('GET', '/api/v1/videos' + (q.size ? '?' + q : ''));
259
363
  out(d, (d.videos || []).map((v) => ' ' + String(v.id).padEnd(8) + v.status.padEnd(10) + (v.tool || '').padEnd(18) + dim(v.name || '')).join('\n') || ' (no videos yet)');
260
364
  },
@@ -278,6 +382,7 @@ Commands:
278
382
  login Save and verify your API key (~/.facelessad, 0600)
279
383
  balance Plan and credit balance
280
384
  tools [--tool <id>] Tool registry — styles, structures, durations
385
+ brands Your brands and their ids (for --brand-kit)
281
386
  voices [--language --gender]Curated voice pool
282
387
  estimate <create-flags> Upper-bound credit cost, without creating
283
388
  create --tool <id> ... Create a video (returns an id immediately)
@@ -289,8 +394,17 @@ Create flags:
289
394
  --tool --url --text --duration --aspect --language --style --style-hint
290
395
  --structure --hook --video-mode --brand-color --brand-name --cta --name
291
396
  --voice --gender --no-voice --music --sfx --captions --product-image
397
+ --no-brand-kit --no-winners --brand-kit <id> (see: facelessad brands)
292
398
  --file body.json (base body; flags override) --dry-run (print, don't send)
293
399
 
400
+ --wait gives up after 30 minutes; --timeout <seconds> changes that. A video
401
+ in "draft" is not building and is never waited on.
402
+
403
+ Accepted spellings: --brand = --brand-name, --color = --brand-color,
404
+ --voice-id = --voice, --voice-gender = --gender, --no-voice-over = --no-voice,
405
+ --no-captions = --captions false, --no-music = --music false, -o = --out.
406
+ An unknown flag is an error, not something quietly ignored.
407
+
294
408
  Every command accepts --json. FACELESSAD_API_KEY wins over the saved key
295
409
  (use it in CI); long builds are better served by a webhook than --wait —
296
410
  see ${API}/developers.
@@ -306,8 +420,39 @@ function statusLine(d) {
306
420
  return s;
307
421
  }
308
422
 
423
+ /**
424
+ * 1.0.1 — kaksi korjausta odotussilmukkaan.
425
+ *
426
+ * 1. AIKAKATTO. Silmukka oli `for(;;)` ilman ulospääsyä muuten kuin
427
+ * done/failed. Jos video jäi mihin tahansa muuhun tilaan, komento jäi
428
+ * pyörimään ikuisesti — CI-ajossa se on jumittunut job, ei virhe.
429
+ * 2. `draft` ON TERMINAALINEN. Palvelin palauttaa sen kansiolle joka ei
430
+ * rakennu eikä valmistu itsestään (appissa aloitettu luonnos, tai ajo
431
+ * jonka etenemistieto on vanhentunut). Sitä ei ole mitään mieltä odottaa.
432
+ * Aiemmin palvelin palautti tässä tilanteessa dokumentoimattoman arvon
433
+ * `active`, joka ei ollut done eikä failed — eli juuri se tapaus joka
434
+ * jäädytti silmukan.
435
+ *
436
+ * Oletuskatto on 30 min; --timeout <sekuntia> muuttaa sen. Aikakatto poistuu
437
+ * koodilla 1, koska pipeline ei saa jatkaa videolla jota ei ole.
438
+ */
439
+ const WAIT_TIMEOUT_DEFAULT = 1800;
440
+ const POLL_SECONDS = 5;
441
+
309
442
  async function waitFor(id) {
310
443
  const started = Date.now();
444
+ // 1.0.1: --timeout luetaan sellaisenaan. Ensimmäinen versio tästä pakotti
445
+ // lattian 30 s:aan, jolloin `--timeout 12` odotti 30 s eikä kertonut siitä
446
+ // — sama hiljainen ylikirjoitus jota tämä paketti on täynnä korjaamassa.
447
+ // Nyt kelpaamaton arvo on VIRHE ja kelvollinen tehdään sellaisenaan;
448
+ // alaraja on yksi pollausväli, koska sitä lyhyempi ei ehdi kysyä kertaakaan.
449
+ let limit = WAIT_TIMEOUT_DEFAULT;
450
+ if (flags.timeout !== undefined && flags.timeout !== true) {
451
+ limit = Number(flags.timeout);
452
+ if (!Number.isFinite(limit) || limit < POLL_SECONDS) {
453
+ die('--timeout must be a number of seconds, at least ' + POLL_SECONDS, 'invalid_timeout');
454
+ }
455
+ }
311
456
  let last = '';
312
457
  for (;;) {
313
458
  const d = await call('GET', '/api/v1/videos/' + encodeURIComponent(id));
@@ -323,7 +468,21 @@ async function waitFor(id) {
323
468
  else { process.stderr.write('Error: video ' + id + ' failed' + (d.error ? ': ' + d.error : '') + '\n'); }
324
469
  process.exit(1);
325
470
  }
326
- await new Promise((r) => setTimeout(r, 5000));
471
+ if (d.status === 'draft') {
472
+ const msg = 'Video ' + id + ' is a draft — it is not building and will not finish on its own. '
473
+ + 'Open it in the app, or start a new build.';
474
+ if (asJson) { process.stdout.write(JSON.stringify(d) + '\n'); }
475
+ else { process.stderr.write('Error: ' + msg + '\n'); }
476
+ process.exit(1);
477
+ }
478
+ if (elapsed + POLL_SECONDS > limit) {
479
+ const msg = 'Gave up waiting for video ' + id + ' after ' + elapsed + 's (last status: ' + d.status
480
+ + (d.phase ? ' — ' + d.phase : '') + '). The build may still finish; check with: facelessad status ' + id;
481
+ if (asJson) { process.stdout.write(JSON.stringify({ ...d, ok: false, code: 'wait_timeout' }) + '\n'); }
482
+ else { process.stderr.write('Error: ' + msg + '\n' + dim('wait_timeout') + '\n'); }
483
+ process.exit(1);
484
+ }
485
+ await new Promise((r) => setTimeout(r, POLL_SECONDS * 1000));
327
486
  }
328
487
  }
329
488
 
@@ -331,4 +490,11 @@ async function waitFor(id) {
331
490
 
332
491
  const fn = commands[cmd];
333
492
  if (!fn) die('Unknown command "' + cmd + '". Run: facelessad help', 'unknown_command');
493
+ // 1.0.1: tarkistus ennen komennon ajoa — help ja version saavat mennä läpi,
494
+ // jotta väärin kirjoitetun lipun jälkeen pääsee lukemaan mitkä ovat oikein.
495
+ if (unknownFlags.length && cmd !== 'help' && cmd !== 'version') {
496
+ die('Unknown flag' + (unknownFlags.length > 1 ? 's' : '') + ': '
497
+ + unknownFlags.join(' ')
498
+ + '. Run: facelessad help', 'unknown_flag');
499
+ }
334
500
  fn().catch((e) => die(e.message || String(e), 'unexpected'));
package/package.json CHANGED
@@ -1,12 +1,25 @@
1
1
  {
2
2
  "name": "@facelessad/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Create faceless video ads from your terminal or build scripts — the FacelessAd command line.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
- "bin": { "facelessad": "index.js" },
8
- "files": ["index.js", "README.md"],
9
- "engines": { "node": ">=18" },
10
- "keywords": ["facelessad", "video", "ads", "ai", "cli"],
7
+ "bin": {
8
+ "facelessad": "index.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "keywords": [
18
+ "facelessad",
19
+ "video",
20
+ "ads",
21
+ "ai",
22
+ "cli"
23
+ ],
11
24
  "homepage": "https://facelessad.com/developers"
12
25
  }