@flatkey-ai/cli 0.1.4 → 0.1.6

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.
@@ -1,204 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { createServer } from "node:http";
3
- import { spawn } from "node:child_process";
4
- import { mkdtemp, readFile } from "node:fs/promises";
5
- import { tmpdir } from "node:os";
6
- import { join } from "node:path";
7
- import { test } from "node:test";
8
-
9
- const BIN = new URL("../bin/flatkey.js", import.meta.url).pathname;
10
-
11
- test("runs generation and utility commands in json mode", async (t) => {
12
- const requests = [];
13
- const server = createServer((request, response) => {
14
- let body = "";
15
- request.on("data", (chunk) => {
16
- body += chunk;
17
- });
18
- request.on("end", () => {
19
- requests.push({ url: request.url, method: request.method, body });
20
- response.setHeader("content-type", "application/json");
21
- if (request.url.startsWith("/v1beta/models/")) {
22
- response.end(JSON.stringify({ data: [{ url: "https://cdn.test/image.png" }] }));
23
- } else if (request.url === "/v1/video/generations") {
24
- response.end(JSON.stringify({ data: [{ url: "https://cdn.test/video.mp4" }] }));
25
- } else if (request.url === "/v1/audio/generations") {
26
- response.end(JSON.stringify({ data: [{ url: "https://cdn.test/audio.mp3" }] }));
27
- } else if (request.url === "/v1/chat/completions") {
28
- response.end(JSON.stringify({ choices: [{ message: { content: "headline" } }] }));
29
- } else if (request.url === "/v1/credits") {
30
- response.end(JSON.stringify({ remaining: 42, used: 8 }));
31
- } else if (request.url === "/v1/status") {
32
- response.end(JSON.stringify({ status: "ok" }));
33
- } else if (request.url === "/v1/available_models") {
34
- response.end(JSON.stringify({
35
- success: true,
36
- object: "list",
37
- data: [{ id: "remote-image", object: "model", owned_by: "flatkey" }],
38
- }));
39
- } else {
40
- response.statusCode = 404;
41
- response.end(JSON.stringify({ error: { message: "not found" } }));
42
- }
43
- });
44
- });
45
- t.after(() => server.close());
46
- await listen(server);
47
- const baseUrl = `http://127.0.0.1:${server.address().port}`;
48
-
49
- const common = ["--base-url", baseUrl, "--api-key", "test-key", "--json"];
50
- const image = await runCli(["image", "generate", "--prompt", "poster", ...common]);
51
- const video = await runCli(["video", "generate", "--prompt", "clip", ...common]);
52
- const audio = await runCli(["audio", "generate", "--prompt", "voice", ...common]);
53
- const text = await runCli(["text", "generate", "--prompt", "headline", ...common]);
54
- const credits = await runCli(["credits", ...common]);
55
- const status = await runCli(["status", ...common]);
56
- const models = await runCli(["models", ...common]);
57
-
58
- assert.deepEqual(JSON.parse(image.stdout).artifacts, [{ url: "https://cdn.test/image.png" }]);
59
- assert.deepEqual(JSON.parse(video.stdout).artifacts, [{ url: "https://cdn.test/video.mp4" }]);
60
- assert.deepEqual(JSON.parse(audio.stdout).artifacts, [{ url: "https://cdn.test/audio.mp3" }]);
61
- assert.equal(JSON.parse(text.stdout).text, "headline");
62
- assert.equal(JSON.parse(credits.stdout).remaining, 42);
63
- assert.equal(JSON.parse(status.stdout).status, "ok");
64
- assert.deepEqual(JSON.parse(models.stdout).models, [
65
- { id: "remote-image", type: "image", source: "remote" },
66
- ]);
67
- assert.equal(image.stderr, "");
68
- assert.ok(requests.some((request) => request.url.startsWith("/v1beta/models/")));
69
- assert.ok(requests.some((request) => request.url === "/v1/video/generations"));
70
- assert.ok(requests.some((request) => request.url === "/v1/audio/generations"));
71
- assert.ok(requests.some((request) => request.url === "/v1/chat/completions"));
72
- });
73
-
74
- test("dry-run returns planned request without calling network", async () => {
75
- const result = await runCli([
76
- "image",
77
- "generate",
78
- "--prompt",
79
- "poster",
80
- "--model",
81
- "gpt-image-2",
82
- "--dry-run",
83
- "--json",
84
- ]);
85
-
86
- const payload = JSON.parse(result.stdout);
87
- assert.equal(payload.dryRun, true);
88
- assert.equal(payload.kind, "image");
89
- assert.equal(payload.request.url, "https://router.flatkey.ai/v1/images/generations");
90
- assert.equal(payload.request.body.model, "gpt-image-2");
91
- assert.equal(result.stderr, "");
92
- });
93
-
94
- test("generation commands write explicit output files", async (t) => {
95
- const server = createServer((request, response) => {
96
- request.on("data", () => {});
97
- request.on("end", () => {
98
- response.setHeader("content-type", "application/json");
99
- if (request.url === "/v1/images/generations") {
100
- response.end(JSON.stringify({
101
- data: [{ b64_json: Buffer.from("image-file").toString("base64") }],
102
- }));
103
- } else if (request.url === "/v1/chat/completions") {
104
- response.end(JSON.stringify({ choices: [{ message: { content: "text-file" } }] }));
105
- } else {
106
- response.statusCode = 404;
107
- response.end(JSON.stringify({ error: { message: "not found" } }));
108
- }
109
- });
110
- });
111
- t.after(() => server.close());
112
- await listen(server);
113
- const baseUrl = `http://127.0.0.1:${server.address().port}`;
114
- const dir = await mkdtemp(join(tmpdir(), "flatkey-output-"));
115
- const imageOutput = join(dir, "poster.png");
116
- const textOutput = join(dir, "headline.txt");
117
-
118
- const common = ["--base-url", baseUrl, "--api-key", "test-key", "--json"];
119
- const image = await runCli([
120
- "image",
121
- "generate",
122
- "--model",
123
- "gpt-image-2",
124
- "--prompt",
125
- "poster",
126
- "--output",
127
- imageOutput,
128
- ...common,
129
- ]);
130
- const text = await runCli([
131
- "text",
132
- "generate",
133
- "--prompt",
134
- "headline",
135
- "-o",
136
- textOutput,
137
- ...common,
138
- ]);
139
-
140
- assert.deepEqual(JSON.parse(image.stdout).artifacts, [{ path: imageOutput }]);
141
- assert.equal(await readFile(imageOutput, "utf8"), "image-file");
142
- assert.equal(JSON.parse(text.stdout).output, textOutput);
143
- assert.equal(await readFile(textOutput, "utf8"), "text-file");
144
- });
145
-
146
- test("prints json errors to stderr in json mode", async () => {
147
- const result = await runCliAllowFailure(["credits", "--json"]);
148
-
149
- assert.equal(result.code, 1);
150
- assert.equal(result.stdout, "");
151
- assert.deepEqual(JSON.parse(result.stderr), {
152
- error: {
153
- message: "Missing Flatkey API key. Run `flatkey onboard --api-key <key>` or set FLATKEY_API_KEY.",
154
- },
155
- });
156
- });
157
-
158
- function listen(server) {
159
- return new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
160
- }
161
-
162
- function runCli(args) {
163
- return new Promise((resolve, reject) => {
164
- const child = spawn(process.execPath, [BIN, ...args], {
165
- env: { ...process.env, FLATKEY_API_KEY: "" },
166
- });
167
- let stdout = "";
168
- let stderr = "";
169
- child.stdout.on("data", (chunk) => {
170
- stdout += chunk;
171
- });
172
- child.stderr.on("data", (chunk) => {
173
- stderr += chunk;
174
- });
175
- child.on("error", reject);
176
- child.on("close", (code) => {
177
- if (code !== 0) {
178
- reject(new Error(`CLI exited ${code}\nstdout:${stdout}\nstderr:${stderr}`));
179
- return;
180
- }
181
- resolve({ stdout, stderr });
182
- });
183
- });
184
- }
185
-
186
- function runCliAllowFailure(args) {
187
- return new Promise((resolve, reject) => {
188
- const child = spawn(process.execPath, [BIN, ...args], {
189
- env: { ...process.env, FLATKEY_API_KEY: "" },
190
- });
191
- let stdout = "";
192
- let stderr = "";
193
- child.stdout.on("data", (chunk) => {
194
- stdout += chunk;
195
- });
196
- child.stderr.on("data", (chunk) => {
197
- stderr += chunk;
198
- });
199
- child.on("error", reject);
200
- child.on("close", (code) => {
201
- resolve({ code, stdout, stderr });
202
- });
203
- });
204
- }
@@ -1,91 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdtemp } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { spawn } from "node:child_process";
6
- import { test } from "node:test";
7
-
8
- const BIN = new URL("../bin/flatkey.js", import.meta.url).pathname;
9
- const LIVE = process.env.FLATKEY_LIVE_TESTS === "1";
10
-
11
- const cases = [
12
- {
13
- name: "models list",
14
- args: ["models", "--json"],
15
- paid: false,
16
- expect: (payload) => {
17
- assert.ok(Array.isArray(payload.models));
18
- assert.ok(payload.models.length > 0);
19
- },
20
- },
21
- {
22
- name: "image gpt",
23
- args: ["image", "generate", "--model", "gpt-image-2", "--prompt", "one tiny red square icon", "--json"],
24
- paid: true,
25
- dryRun: false,
26
- expect: (payload) => assert.equal(payload.kind, "image"),
27
- },
28
- {
29
- name: "image nano",
30
- args: ["image", "generate", "--model", "nano-banana-pro-preview", "--prompt", "one tiny blue square icon", "--json"],
31
- paid: true,
32
- dryRun: false,
33
- expect: (payload) => assert.equal(payload.kind, "image"),
34
- },
35
- {
36
- name: "video seedance2",
37
- args: ["video", "generate", "--model", "seedance2", "--prompt", "one second newsroom establishing shot", "--duration", "1", "--json"],
38
- paid: true,
39
- dryRun: false,
40
- expect: (payload) => assert.equal(payload.kind, "video"),
41
- },
42
- {
43
- name: "text gpt-5.5",
44
- args: ["text", "generate", "--model", "gpt-5.5", "--prompt", "Write a five word headline.", "--json"],
45
- paid: true,
46
- dryRun: false,
47
- expect: (payload) => {
48
- assert.equal(payload.kind, "text");
49
- assert.equal(typeof payload.text, "string");
50
- },
51
- },
52
- ];
53
-
54
- for (const item of cases) {
55
- test(`live flatkey ${item.name}${item.dryRun ? " dry-run" : ""}`, { skip: !LIVE }, async () => {
56
- assert.ok(process.env.FLATKEY_API_KEY, "FLATKEY_API_KEY is required");
57
- const outDir = await mkdtemp(join(tmpdir(), "flatkey-live-"));
58
- const args = [...item.args, "--out", outDir];
59
- if (item.dryRun) args.push("--dry-run");
60
- const result = await runCli(args);
61
- const payload = JSON.parse(result.stdout);
62
- item.expect(payload);
63
- });
64
- }
65
-
66
- function runCli(args) {
67
- return new Promise((resolve, reject) => {
68
- const child = spawn(process.execPath, [BIN, ...args], {
69
- env: {
70
- ...process.env,
71
- FLATKEY_API_KEY: process.env.FLATKEY_API_KEY,
72
- },
73
- });
74
- let stdout = "";
75
- let stderr = "";
76
- child.stdout.on("data", (chunk) => {
77
- stdout += chunk;
78
- });
79
- child.stderr.on("data", (chunk) => {
80
- stderr += chunk;
81
- });
82
- child.on("error", reject);
83
- child.on("close", (code) => {
84
- if (code !== 0) {
85
- reject(new Error(`CLI exited ${code}\nstdout:${stdout}\nstderr:${stderr}`));
86
- return;
87
- }
88
- resolve({ stdout, stderr });
89
- });
90
- });
91
- }
@@ -1,56 +0,0 @@
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.id === "seedance2"));
11
- assert.ok(models.some((model) => model.id === "gpt-5.5"));
12
- assert.ok(models.some((model) => model.type === "video"));
13
- assert.ok(models.some((model) => model.type === "text"));
14
- assert.ok(models.some((model) => model.type === "audio"));
15
- assert.equal(models[0].source, "bundled");
16
- });
17
-
18
- test("filters bundled models by type", () => {
19
- const models = getBundledModels("image");
20
-
21
- assert.ok(models.length > 0);
22
- assert.ok(models.every((model) => model.type === "image"));
23
- });
24
-
25
- test("normalizes remote model arrays", () => {
26
- const models = normalizeModels({
27
- data: [
28
- { id: "img-x", type: "image" },
29
- { id: "vid-x", capabilities: ["video"] },
30
- ],
31
- });
32
-
33
- assert.deepEqual(models, [
34
- { id: "img-x", type: "image", source: "remote" },
35
- { id: "vid-x", type: "video", source: "remote" },
36
- ]);
37
- });
38
-
39
- test("normalizes OpenAI model list response from /v1/models", () => {
40
- const models = normalizeModels({
41
- object: "list",
42
- data: [
43
- { id: "gpt-5.5", object: "model" },
44
- { id: "seedance2", object: "model" },
45
- { id: "gpt-image-2", object: "model" },
46
- { id: "nano-banana-pro-preview", object: "model" },
47
- ],
48
- });
49
-
50
- assert.deepEqual(models, [
51
- { id: "gpt-5.5", type: "text", source: "remote" },
52
- { id: "seedance2", type: "video", source: "remote" },
53
- { id: "gpt-image-2", type: "image", source: "remote" },
54
- { id: "nano-banana-pro-preview", type: "image", source: "remote" },
55
- ]);
56
- });
@@ -1,29 +0,0 @@
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
- });