@flatkey-ai/cli 0.1.11 → 0.1.13

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/package.json +1 -1
  2. package/src/cli.js +147 -18
  3. package/src/config.js +34 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flatkey-ai/cli",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Flatkey media generation CLI for image, video, audio, credits, status, and models.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -215,14 +215,17 @@ export async function runCommand(command, deps = {}) {
215
215
  }
216
216
 
217
217
  async function handleLogin(command, deps) {
218
- const { ensureDeviceId, writeAuthConfig } = await import("./config.js");
218
+ const { ensureDeviceId, resolveOrigins, writeAuthConfig } = await import("./config.js");
219
219
  const { createDeviceAuthorization, pollDeviceAuthorization } = await import("./api.js");
220
220
  const deviceId = await ensureDeviceId({ configDir: deps.configDir });
221
221
  const version = await readPackageVersion();
222
- const env = deps.env ?? process.env;
223
- const consoleUrl = firstNonEmpty(command.options.console_url, env.CONSOLE_ORIGIN);
222
+ const { consoleOrigin } = await resolveOrigins({
223
+ consoleUrl: command.options.console_url,
224
+ env: deps.env ?? process.env,
225
+ configDir: deps.configDir,
226
+ });
224
227
  const authorization = await createDeviceAuthorization({
225
- consoleUrl,
228
+ consoleUrl: consoleOrigin,
226
229
  deviceId,
227
230
  clientName: "flatkey-cli",
228
231
  clientVersion: version,
@@ -246,7 +249,7 @@ async function handleLogin(command, deps) {
246
249
  while (Date.now() < deadline) {
247
250
  await delay(nextLoginPollDelay(startedAt, initialIntervalMs), deps);
248
251
  const poll = await pollDeviceAuthorization({
249
- consoleUrl,
252
+ consoleUrl: consoleOrigin,
250
253
  deviceCode: data.device_code,
251
254
  fetch: deps.fetch,
252
255
  });
@@ -352,10 +355,10 @@ async function handleAuthStatus(command, deps) {
352
355
  authenticated: Boolean(apiKey),
353
356
  source: command.options.api_key
354
357
  ? "option"
355
- : (deps.env ?? process.env).FLATKEY_API_KEY
356
- ? "env"
357
- : saved?.apiKey
358
- ? "config"
358
+ : saved?.apiKey
359
+ ? "config"
360
+ : (deps.env ?? process.env).FLATKEY_API_KEY
361
+ ? "env"
359
362
  : "none",
360
363
  key: maskKey(apiKey),
361
364
  auth: saved?.auth ?? null,
@@ -364,7 +367,7 @@ async function handleAuthStatus(command, deps) {
364
367
  }
365
368
 
366
369
  async function handleGenerate(command, deps) {
367
- const { resolveApiKey } = await import("./config.js");
370
+ const { resolveApiKey, resolveOrigins } = await import("./config.js");
368
371
  const {
369
372
  generateAudio,
370
373
  generateAudioMusic,
@@ -386,11 +389,17 @@ async function handleGenerate(command, deps) {
386
389
  : await resolveApiKey({
387
390
  apiKey: command.options.api_key,
388
391
  env: deps.env ?? process.env,
392
+ configDir: deps.configDir,
389
393
  });
394
+ const { routerOrigin } = await resolveOrigins({
395
+ baseUrl: command.options.base_url,
396
+ env: deps.env ?? process.env,
397
+ configDir: deps.configDir,
398
+ });
390
399
  const options = {
391
400
  ...command.options,
392
401
  apiKey,
393
- baseUrl: firstNonEmpty(command.options.base_url, (deps.env ?? process.env).ROUTER_ORIGIN),
402
+ baseUrl: routerOrigin,
394
403
  fetch: deps.fetch,
395
404
  };
396
405
  if (command.options.dry_run) {
@@ -464,15 +473,21 @@ function scrubArtifactResponse(value) {
464
473
  }
465
474
 
466
475
  async function handleVoices(command, deps) {
467
- const { resolveApiKey } = await import("./config.js");
476
+ const { resolveApiKey, resolveOrigins } = await import("./config.js");
468
477
  const { getVoices } = await import("./api.js");
469
478
  const apiKey = await resolveApiKey({
470
479
  apiKey: command.options.api_key,
471
480
  env: deps.env ?? process.env,
481
+ configDir: deps.configDir,
482
+ });
483
+ const { routerOrigin } = await resolveOrigins({
484
+ baseUrl: command.options.base_url,
485
+ env: deps.env ?? process.env,
486
+ configDir: deps.configDir,
472
487
  });
473
488
  return getVoices({
474
489
  apiKey,
475
- baseUrl: firstNonEmpty(command.options.base_url, (deps.env ?? process.env).ROUTER_ORIGIN),
490
+ baseUrl: routerOrigin,
476
491
  fetch: deps.fetch,
477
492
  });
478
493
  }
@@ -516,32 +531,44 @@ function redactRequest(request) {
516
531
  }
517
532
 
518
533
  async function handleUtility(command, deps) {
519
- const { resolveApiKey } = await import("./config.js");
534
+ const { resolveApiKey, resolveOrigins } = await import("./config.js");
520
535
  const { getCredits, getStatus } = await import("./api.js");
521
536
  const apiKey = await resolveApiKey({
522
537
  apiKey: command.options.api_key,
523
538
  env: deps.env ?? process.env,
539
+ configDir: deps.configDir,
540
+ });
541
+ const { consoleOrigin } = await resolveOrigins({
542
+ baseUrl: command.options.base_url,
543
+ env: deps.env ?? process.env,
544
+ configDir: deps.configDir,
524
545
  });
525
546
  const options = {
526
547
  apiKey,
527
- baseUrl: firstNonEmpty(command.options.base_url, (deps.env ?? process.env).CONSOLE_ORIGIN),
548
+ baseUrl: consoleOrigin,
528
549
  fetch: deps.fetch,
529
550
  };
530
551
  return command.group === "credits" ? getCredits(options) : getStatus(options);
531
552
  }
532
553
 
533
554
  async function handleModels(command, deps) {
534
- const { resolveApiKey } = await import("./config.js");
555
+ const { resolveApiKey, resolveOrigins } = await import("./config.js");
535
556
  const { getModels } = await import("./api.js");
536
557
  const { normalizeModels } = await import("./models.js");
537
558
 
538
559
  const apiKey = await resolveApiKey({
539
560
  apiKey: command.options.api_key,
540
561
  env: deps.env ?? process.env,
562
+ configDir: deps.configDir,
563
+ });
564
+ const { consoleOrigin } = await resolveOrigins({
565
+ baseUrl: command.options.base_url,
566
+ env: deps.env ?? process.env,
567
+ configDir: deps.configDir,
541
568
  });
542
569
  const response = await getModels({
543
570
  apiKey,
544
- baseUrl: firstNonEmpty(command.options.base_url, (deps.env ?? process.env).CONSOLE_ORIGIN),
571
+ baseUrl: consoleOrigin,
545
572
  fetch: deps.fetch,
546
573
  });
547
574
  return { models: normalizeModels(response, command.options.type) };
@@ -549,7 +576,109 @@ async function handleModels(command, deps) {
549
576
 
550
577
  function formatHuman(result) {
551
578
  if (typeof result === "string") return result;
552
- return JSON.stringify(result, null, 2);
579
+ if (!result || typeof result !== "object") return String(result);
580
+ if (result.dryRun && result.request) return formatDryRun(result);
581
+ if (result.kind) return formatGenerationResult(result);
582
+ if (Array.isArray(result.models)) return formatModels(result.models);
583
+ if (Array.isArray(result.voices)) return formatVoices(result.voices);
584
+ if (typeof result.authenticated === "boolean") return formatAuthStatus(result);
585
+ return formatKeyValues(result);
586
+ }
587
+
588
+ function formatDryRun(result) {
589
+ const lines = [
590
+ `Dry run: ${result.kind}`,
591
+ `${result.request.method ?? "GET"} ${result.request.url}`,
592
+ ];
593
+ if (result.request.body !== undefined) {
594
+ lines.push("Body:");
595
+ lines.push(JSON.stringify(result.request.body, null, 2));
596
+ }
597
+ return lines.join("\n");
598
+ }
599
+
600
+ function formatGenerationResult(result) {
601
+ if (result.kind === "text") {
602
+ const lines = [result.text || "(empty text)"];
603
+ if (result.output) lines.push(`Saved: ${result.output}`);
604
+ return lines.join("\n");
605
+ }
606
+ const artifacts = Array.isArray(result.artifacts) ? result.artifacts : [];
607
+ if (artifacts.length === 0) return `${titleCase(result.kind)} generated. No artifacts returned.`;
608
+ const lines = [`${titleCase(result.kind)} generated:`];
609
+ for (const artifact of artifacts) {
610
+ if (artifact.path) lines.push(`- Saved: ${artifact.path}`);
611
+ else if (artifact.url) lines.push(`- URL: ${artifact.url}`);
612
+ else lines.push(`- ${JSON.stringify(artifact)}`);
613
+ }
614
+ return lines.join("\n");
615
+ }
616
+
617
+ function formatModels(models) {
618
+ if (models.length === 0) return "No models available.";
619
+ return [
620
+ `Available models (${models.length}):`,
621
+ formatTable(
622
+ ["Model", "Type", "Source"],
623
+ models.map((model) => [
624
+ model.id,
625
+ model.type ?? "",
626
+ model.source ?? "",
627
+ ]),
628
+ ),
629
+ ].join("\n");
630
+ }
631
+
632
+ function formatVoices(voices) {
633
+ if (voices.length === 0) return "No voices available.";
634
+ const lines = [`Available voices (${voices.length}):`];
635
+ for (const voice of voices) {
636
+ const id = voice.voice_id ?? voice.id;
637
+ const name = voice.name ? `${voice.name} ` : "";
638
+ lines.push(`- ${name}${id ? `(${id})` : ""}`.trim());
639
+ }
640
+ return lines.join("\n");
641
+ }
642
+
643
+ function formatKeyValues(result) {
644
+ const entries = Object.entries(result);
645
+ if (entries.length === 0) return "OK";
646
+ return entries
647
+ .map(([key, value]) => `${humanLabel(key)}: ${formatScalar(value)}`)
648
+ .join("\n");
649
+ }
650
+
651
+ function formatScalar(value) {
652
+ if (value === null || value === undefined) return "";
653
+ if (typeof value === "object") return JSON.stringify(value);
654
+ return String(value);
655
+ }
656
+
657
+ function humanLabel(key) {
658
+ return key
659
+ .replaceAll("_", " ")
660
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
661
+ .replace(/^./, (char) => char.toUpperCase());
662
+ }
663
+
664
+ function titleCase(value) {
665
+ return String(value).replace(/^./, (char) => char.toUpperCase());
666
+ }
667
+
668
+ function formatTable(headers, rows) {
669
+ const widths = headers.map((header, index) => Math.max(
670
+ header.length,
671
+ ...rows.map((row) => String(row[index] ?? "").length),
672
+ ));
673
+ const line = (cells) => cells
674
+ .map((cell, index) => String(cell ?? "").padEnd(widths[index]))
675
+ .join(" ")
676
+ .trimEnd();
677
+ return [
678
+ line(headers),
679
+ line(widths.map((width) => "-".repeat(width))),
680
+ ...rows.map(line),
681
+ ].join("\n");
553
682
  }
554
683
 
555
684
  async function readPackageVersion() {
package/src/config.js CHANGED
@@ -3,6 +3,9 @@ import { randomUUID } from "node:crypto";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
 
6
+ export const ROUTER_ORIGIN_KEY = "routerOrigin";
7
+ export const CONSOLE_ORIGIN_KEY = "consoleOrigin";
8
+
6
9
  export function getDefaultConfigDir() {
7
10
  return join(homedir(), ".config", "flatkey");
8
11
  }
@@ -17,16 +20,40 @@ export async function resolveApiKey({
17
20
  configDir = getDefaultConfigDir(),
18
21
  } = {}) {
19
22
  if (apiKey) return apiKey;
20
- if (env.FLATKEY_API_KEY) return env.FLATKEY_API_KEY;
21
23
 
22
24
  const saved = await readSavedConfig(configDir);
23
25
  if (saved?.apiKey) return saved.apiKey;
26
+ if (env.FLATKEY_API_KEY) return env.FLATKEY_API_KEY;
24
27
 
25
28
  throw new Error(
26
29
  "Missing Flatkey API key. Create one at https://console.flatkey.ai/keys, then run `flatkey onboard --api-key <key>` or set FLATKEY_API_KEY.",
27
30
  );
28
31
  }
29
32
 
33
+ export async function resolveOrigins({
34
+ baseUrl,
35
+ consoleUrl,
36
+ env = process.env,
37
+ configDir = getDefaultConfigDir(),
38
+ } = {}) {
39
+ const saved = await readSavedConfig(configDir);
40
+ return {
41
+ routerOrigin: firstNonEmpty(
42
+ baseUrl,
43
+ saved?.[ROUTER_ORIGIN_KEY],
44
+ saved?.ROUTER_ORIGIN,
45
+ env.ROUTER_ORIGIN,
46
+ ),
47
+ consoleOrigin: firstNonEmpty(
48
+ consoleUrl,
49
+ baseUrl,
50
+ saved?.[CONSOLE_ORIGIN_KEY],
51
+ saved?.CONSOLE_ORIGIN,
52
+ env.CONSOLE_ORIGIN,
53
+ ),
54
+ };
55
+ }
56
+
30
57
  export async function writeConfig({ apiKey, configDir = getDefaultConfigDir() }) {
31
58
  if (typeof apiKey !== "string" || apiKey.trim() === "") {
32
59
  throw new Error("Missing --api-key value. Create a key at https://console.flatkey.ai/keys, then run `flatkey onboard --api-key <key>`.");
@@ -34,7 +61,8 @@ export async function writeConfig({ apiKey, configDir = getDefaultConfigDir() })
34
61
 
35
62
  await mkdir(configDir, { recursive: true });
36
63
  const configPath = getConfigPath(configDir);
37
- await writeFile(configPath, `${JSON.stringify({ apiKey }, null, 2)}\n`, {
64
+ const saved = await readSavedConfig(configDir) ?? {};
65
+ await writeFile(configPath, `${JSON.stringify({ ...saved, apiKey }, null, 2)}\n`, {
38
66
  mode: 0o600,
39
67
  });
40
68
  try {
@@ -109,3 +137,7 @@ async function readSavedConfig(configDir) {
109
137
  throw error;
110
138
  }
111
139
  }
140
+
141
+ function firstNonEmpty(...values) {
142
+ return values.find((value) => typeof value === "string" && value.trim() !== "");
143
+ }