@flatkey-ai/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.
@@ -0,0 +1,62 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, stat } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { test } from "node:test";
6
+
7
+ import { getConfigPath, resolveApiKey, writeConfig } from "../src/config.js";
8
+
9
+ test("resolves api key from explicit option first", async () => {
10
+ const configDir = await mkdtemp(join(tmpdir(), "flatkey-config-"));
11
+ await writeConfig({ apiKey: "saved-key", configDir });
12
+
13
+ const key = await resolveApiKey({
14
+ apiKey: "option-key",
15
+ env: { FLATKEY_API_KEY: "env-key" },
16
+ configDir,
17
+ });
18
+
19
+ assert.equal(key, "option-key");
20
+ });
21
+
22
+ test("resolves api key from FLATKEY_API_KEY before saved config", async () => {
23
+ const configDir = await mkdtemp(join(tmpdir(), "flatkey-config-"));
24
+ await writeConfig({ apiKey: "saved-key", configDir });
25
+
26
+ const key = await resolveApiKey({
27
+ env: { FLATKEY_API_KEY: "env-key" },
28
+ configDir,
29
+ });
30
+
31
+ assert.equal(key, "env-key");
32
+ });
33
+
34
+ test("resolves api key from saved config", async () => {
35
+ const configDir = await mkdtemp(join(tmpdir(), "flatkey-config-"));
36
+ await writeConfig({ apiKey: "saved-key", configDir });
37
+
38
+ const key = await resolveApiKey({ env: {}, configDir });
39
+
40
+ assert.equal(key, "saved-key");
41
+ });
42
+
43
+ test("throws actionable error when api key is missing", async () => {
44
+ const configDir = await mkdtemp(join(tmpdir(), "flatkey-config-"));
45
+
46
+ await assert.rejects(
47
+ () => resolveApiKey({ env: {}, configDir }),
48
+ /Missing Flatkey API key.*flatkey onboard.*FLATKEY_API_KEY/s,
49
+ );
50
+ });
51
+
52
+ test("writes config json with api key", async () => {
53
+ const configDir = await mkdtemp(join(tmpdir(), "flatkey-config-"));
54
+
55
+ const configPath = await writeConfig({ apiKey: "written-key", configDir });
56
+ const saved = JSON.parse(await readFile(configPath, "utf8"));
57
+ const fileStat = await stat(configPath);
58
+
59
+ assert.equal(configPath, getConfigPath(configDir));
60
+ assert.equal(saved.apiKey, "written-key");
61
+ assert.ok((fileStat.mode & 0o777) === 0o600 || process.platform === "win32");
62
+ });
@@ -0,0 +1,27 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { getAiHelp, getHumanHelp } from "../src/help.js";
5
+
6
+ test("ai help teaches agents setup and command usage", () => {
7
+ const help = getAiHelp();
8
+
9
+ assert.match(help, /FLATKEY_API_KEY/);
10
+ assert.match(help, /flatkey onboard --api-key/);
11
+ assert.match(help, /flatkey image generate/);
12
+ assert.match(help, /flatkey video generate/);
13
+ assert.match(help, /flatkey audio generate/);
14
+ assert.match(help, /flatkey credits --json/);
15
+ assert.match(help, /flatkey status --json/);
16
+ assert.match(help, /flatkey models --json/);
17
+ assert.match(help, /JSON mode/);
18
+ assert.match(help, /Missing key/);
19
+ });
20
+
21
+ test("human help lists core commands", () => {
22
+ const help = getHumanHelp();
23
+
24
+ assert.match(help, /Usage: flatkey/);
25
+ assert.match(help, /image generate/);
26
+ assert.match(help, /models/);
27
+ });
@@ -0,0 +1,120 @@
1
+ import assert from "node:assert/strict";
2
+ import { createServer } from "node:http";
3
+ import { spawn } from "node:child_process";
4
+ import { test } from "node:test";
5
+
6
+ const BIN = new URL("../bin/flatkey.js", import.meta.url).pathname;
7
+
8
+ test("runs generation and utility commands in json mode", async (t) => {
9
+ const requests = [];
10
+ const server = createServer((request, response) => {
11
+ let body = "";
12
+ request.on("data", (chunk) => {
13
+ body += chunk;
14
+ });
15
+ request.on("end", () => {
16
+ requests.push({ url: request.url, method: request.method, body });
17
+ response.setHeader("content-type", "application/json");
18
+ if (request.url.startsWith("/v1beta/models/")) {
19
+ response.end(JSON.stringify({ data: [{ url: "https://cdn.test/image.png" }] }));
20
+ } else if (request.url === "/v1/videos/generations") {
21
+ response.end(JSON.stringify({ data: [{ url: "https://cdn.test/video.mp4" }] }));
22
+ } else if (request.url === "/v1/audio/generations") {
23
+ response.end(JSON.stringify({ data: [{ url: "https://cdn.test/audio.mp3" }] }));
24
+ } else if (request.url === "/v1/credits") {
25
+ response.end(JSON.stringify({ remaining: 42, used: 8 }));
26
+ } else if (request.url === "/v1/status") {
27
+ response.end(JSON.stringify({ status: "ok" }));
28
+ } else if (request.url === "/v1/models") {
29
+ response.end(JSON.stringify({ data: [{ id: "remote-image", type: "image" }] }));
30
+ } else {
31
+ response.statusCode = 404;
32
+ response.end(JSON.stringify({ error: { message: "not found" } }));
33
+ }
34
+ });
35
+ });
36
+ t.after(() => server.close());
37
+ await listen(server);
38
+ const baseUrl = `http://127.0.0.1:${server.address().port}`;
39
+
40
+ const common = ["--base-url", baseUrl, "--api-key", "test-key", "--json"];
41
+ const image = await runCli(["image", "generate", "--prompt", "poster", ...common]);
42
+ const video = await runCli(["video", "generate", "--prompt", "clip", ...common]);
43
+ const audio = await runCli(["audio", "generate", "--prompt", "voice", ...common]);
44
+ const credits = await runCli(["credits", ...common]);
45
+ const status = await runCli(["status", ...common]);
46
+ const models = await runCli(["models", ...common]);
47
+
48
+ assert.deepEqual(JSON.parse(image.stdout).artifacts, [{ url: "https://cdn.test/image.png" }]);
49
+ assert.deepEqual(JSON.parse(video.stdout).artifacts, [{ url: "https://cdn.test/video.mp4" }]);
50
+ assert.deepEqual(JSON.parse(audio.stdout).artifacts, [{ url: "https://cdn.test/audio.mp3" }]);
51
+ assert.equal(JSON.parse(credits.stdout).remaining, 42);
52
+ assert.equal(JSON.parse(status.stdout).status, "ok");
53
+ assert.deepEqual(JSON.parse(models.stdout).models, [
54
+ { id: "remote-image", type: "image", source: "remote" },
55
+ ]);
56
+ assert.equal(image.stderr, "");
57
+ assert.ok(requests.some((request) => request.url.startsWith("/v1beta/models/")));
58
+ assert.ok(requests.some((request) => request.url === "/v1/videos/generations"));
59
+ assert.ok(requests.some((request) => request.url === "/v1/audio/generations"));
60
+ });
61
+
62
+ test("prints json errors to stderr in json mode", async () => {
63
+ const result = await runCliAllowFailure(["credits", "--json"]);
64
+
65
+ assert.equal(result.code, 1);
66
+ assert.equal(result.stdout, "");
67
+ assert.deepEqual(JSON.parse(result.stderr), {
68
+ error: {
69
+ message: "Missing Flatkey API key. Run `flatkey onboard --api-key <key>` or set FLATKEY_API_KEY.",
70
+ },
71
+ });
72
+ });
73
+
74
+ function listen(server) {
75
+ return new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
76
+ }
77
+
78
+ function runCli(args) {
79
+ return new Promise((resolve, reject) => {
80
+ const child = spawn(process.execPath, [BIN, ...args], {
81
+ env: { ...process.env, FLATKEY_API_KEY: "" },
82
+ });
83
+ let stdout = "";
84
+ let stderr = "";
85
+ child.stdout.on("data", (chunk) => {
86
+ stdout += chunk;
87
+ });
88
+ child.stderr.on("data", (chunk) => {
89
+ stderr += chunk;
90
+ });
91
+ child.on("error", reject);
92
+ child.on("close", (code) => {
93
+ if (code !== 0) {
94
+ reject(new Error(`CLI exited ${code}\nstdout:${stdout}\nstderr:${stderr}`));
95
+ return;
96
+ }
97
+ resolve({ stdout, stderr });
98
+ });
99
+ });
100
+ }
101
+
102
+ function runCliAllowFailure(args) {
103
+ return new Promise((resolve, reject) => {
104
+ const child = spawn(process.execPath, [BIN, ...args], {
105
+ env: { ...process.env, FLATKEY_API_KEY: "" },
106
+ });
107
+ let stdout = "";
108
+ let stderr = "";
109
+ child.stdout.on("data", (chunk) => {
110
+ stdout += chunk;
111
+ });
112
+ child.stderr.on("data", (chunk) => {
113
+ stderr += chunk;
114
+ });
115
+ child.on("error", reject);
116
+ child.on("close", (code) => {
117
+ resolve({ code, stdout, stderr });
118
+ });
119
+ });
120
+ }
@@ -0,0 +1,34 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { getBundledModels, normalizeModels } from "../src/models.js";
5
+
6
+ test("returns bundled fallback models with source marker", () => {
7
+ const models = getBundledModels();
8
+
9
+ assert.ok(models.some((model) => model.id === "nano-banana-pro-preview"));
10
+ assert.ok(models.some((model) => model.type === "video"));
11
+ assert.ok(models.some((model) => model.type === "audio"));
12
+ assert.equal(models[0].source, "bundled");
13
+ });
14
+
15
+ test("filters bundled models by type", () => {
16
+ const models = getBundledModels("image");
17
+
18
+ assert.ok(models.length > 0);
19
+ assert.ok(models.every((model) => model.type === "image"));
20
+ });
21
+
22
+ test("normalizes remote model arrays", () => {
23
+ const models = normalizeModels({
24
+ data: [
25
+ { id: "img-x", type: "image" },
26
+ { id: "vid-x", capabilities: ["video"] },
27
+ ],
28
+ });
29
+
30
+ assert.deepEqual(models, [
31
+ { id: "img-x", type: "image", source: "remote" },
32
+ { id: "vid-x", type: "video", source: "remote" },
33
+ ]);
34
+ });
@@ -0,0 +1,29 @@
1
+ import assert from "node:assert/strict";
2
+ import { access, readFile, stat } from "node:fs/promises";
3
+ import { constants } from "node:fs";
4
+ import { test } from "node:test";
5
+
6
+ test("package exposes @flatkey-ai/cli flatkey binary", async () => {
7
+ const pkg = JSON.parse(await readFile("package.json", "utf8"));
8
+ const binStat = await stat("bin/flatkey.js");
9
+
10
+ assert.equal(pkg.name, "@flatkey-ai/cli");
11
+ assert.equal(pkg.bin.flatkey, "bin/flatkey.js");
12
+ assert.ok((binStat.mode & 0o111) !== 0);
13
+ });
14
+
15
+ test("release channel docs exist", async () => {
16
+ await access("README.md", constants.R_OK);
17
+ await access("release/homebrew/flatkey.rb", constants.R_OK);
18
+ await access("release/deb/README.md", constants.R_OK);
19
+ await access("release/msi/README.md", constants.R_OK);
20
+ });
21
+
22
+ test("github workflow publishes npm with secret token", async () => {
23
+ const workflow = await readFile(".github/workflows/npm-publish.yml", "utf8");
24
+
25
+ assert.match(workflow, /on:\n\s+workflow_dispatch:/);
26
+ assert.match(workflow, /tags:\n\s+- "v\*"/);
27
+ assert.match(workflow, /NODE_AUTH_TOKEN: \$\{\{ secrets\.NPM_TOKEN \}\}/);
28
+ assert.match(workflow, /npm publish --access public/);
29
+ });