@calo-design/cli 0.5.0 → 0.6.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.
- package/bin/cli.js +7 -1
- package/bin/imagegen.js +171 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ const path = require("node:path");
|
|
|
18
18
|
const { cmdPush } = require("./mirror-push");
|
|
19
19
|
const { cmdArtPush } = require("./art-push");
|
|
20
20
|
const { cmdShare } = require("./share");
|
|
21
|
+
const { cmdImage } = require("./imagegen");
|
|
21
22
|
const { cmdLogin, cmdLogout, ensureLoggedIn, githubToken, reportEvent, ensureSession, loadSession, BROKER } = require("./login");
|
|
22
23
|
|
|
23
24
|
const ORG = "Calo-Design";
|
|
@@ -48,7 +49,8 @@ const PEERS = [
|
|
|
48
49
|
// subprocess only (see gitTokenEnv). No GitHub account, PAT, or SSH key needed.
|
|
49
50
|
const PKG_SPECS = [
|
|
50
51
|
`git+https://github.com/${ORG}/calo-design-system.git`,
|
|
51
|
-
`git+https://github.com/${ORG}/calo-flows.git
|
|
52
|
+
`git+https://github.com/${ORG}/calo-flows.git`,
|
|
53
|
+
`git+https://github.com/${ORG}/calo-creatives.git`
|
|
52
54
|
];
|
|
53
55
|
|
|
54
56
|
const args = process.argv.slice(2);
|
|
@@ -770,6 +772,9 @@ function help() {
|
|
|
770
772
|
${c.dim(" push --slug x --title \"…\" --owner \"…\" --screenshot path --dry-run --direct")}
|
|
771
773
|
${c.dim("art push <dir>")} upload a bulk image set to the CDN + emit a remote-source manifest (login only)
|
|
772
774
|
${c.dim(" art push ./art/byo --set byo --manifest src/byo-art.ts --dry-run --force --direct")}
|
|
775
|
+
${c.dim("image \"<prompt>\"")} AI image generation via the broker (metered per user; login only)
|
|
776
|
+
${c.dim(" image \"…\" --out hero.png --ref photo.jpg --model google/gemini-3-pro-image --json")}
|
|
777
|
+
${c.dim("image usage")} your monthly image-generation metering (images + USD)
|
|
773
778
|
${c.dim("share")} publish THIS web prototype (React/Vite/CRA/Next) to Cloudflare Pages → share link
|
|
774
779
|
${c.dim(" share --slug x --dir dist --build --project calo-prototypes --dry-run")}
|
|
775
780
|
${c.dim("gallery")} serve the @calo/design-system component gallery locally (live from the runtime)
|
|
@@ -793,6 +798,7 @@ function help() {
|
|
|
793
798
|
else if (cmd === "art" && args[1] === "push") await cmdArtPush(args.slice(2));
|
|
794
799
|
else if (cmd === "art") throw new Error("unknown art command — did you mean `calo-design art push <dir>`?");
|
|
795
800
|
else if (cmd === "share") await cmdShare(args.slice(1));
|
|
801
|
+
else if (cmd === "image") await cmdImage(args.slice(1));
|
|
796
802
|
else if (cmd === "feed") await cmdFeed();
|
|
797
803
|
else if (cmd === "insights") await cmdInsights();
|
|
798
804
|
else if (cmd === "whoami") await cmdWhoami();
|
package/bin/imagegen.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `calo-design image` — metered AI image generation through the Calo broker.
|
|
5
|
+
*
|
|
6
|
+
* calo-design image "prompt" generate → writes ./<slug>.png, prints path + URL + cost
|
|
7
|
+
* calo-design image "prompt" --out hero.png choose the output path
|
|
8
|
+
* calo-design image "prompt" --ref photo.jpg reference/edit input (repeatable, ≤4)
|
|
9
|
+
* calo-design image "prompt" --model <id> pick an allowed model (default: broker's)
|
|
10
|
+
* calo-design image usage your monthly metering (images + USD, caps)
|
|
11
|
+
* --json on either form for machine callers (the design/marketing skills use this)
|
|
12
|
+
*
|
|
13
|
+
* The OpenRouter key lives on the broker only; identity is the normal CLI session,
|
|
14
|
+
* and the broker meters every generated image per user per month. The broker also
|
|
15
|
+
* mirrors each image to the CDN (content-hashed, immutable) and returns that URL —
|
|
16
|
+
* use it directly in prototypes; the local file is for creatives/mockups.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require("node:fs");
|
|
20
|
+
const path = require("node:path");
|
|
21
|
+
const { ensureSession, BROKER } = require("./login");
|
|
22
|
+
|
|
23
|
+
const c = { dim: (s) => `\x1b[2m${s}\x1b[0m`, b: (s) => `\x1b[1m${s}\x1b[0m`, g: (s) => `\x1b[32m${s}\x1b[0m` };
|
|
24
|
+
const log = (s = "") => console.log(s);
|
|
25
|
+
|
|
26
|
+
const MIMES = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" };
|
|
27
|
+
const EXTS = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp" };
|
|
28
|
+
|
|
29
|
+
// --flag value (both `--flag=v` and `--flag v`), collecting repeats.
|
|
30
|
+
function flagAll(args, name) {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (let i = 0; i < args.length; i++) {
|
|
33
|
+
if (args[i] === `--${name}` && args[i + 1] != null) out.push(args[++i]);
|
|
34
|
+
else if (args[i].startsWith(`--${name}=`)) out.push(args[i].split("=").slice(1).join("="));
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
const flag = (args, name) => flagAll(args, name)[0];
|
|
39
|
+
|
|
40
|
+
async function apiGet(pathname, session) {
|
|
41
|
+
let res;
|
|
42
|
+
try {
|
|
43
|
+
res = await fetch(BROKER + pathname, {
|
|
44
|
+
headers: { authorization: `Bearer ${session}` },
|
|
45
|
+
signal: AbortSignal.timeout(15000),
|
|
46
|
+
});
|
|
47
|
+
} catch (e) {
|
|
48
|
+
throw new Error(`can't reach the Calo broker at ${BROKER} (${e.message})`);
|
|
49
|
+
}
|
|
50
|
+
const json = await res.json().catch(() => ({}));
|
|
51
|
+
if (!res.ok) throw new Error(json.error || `${pathname} → HTTP ${res.status}`);
|
|
52
|
+
return json;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function refToDataUrl(file) {
|
|
56
|
+
const mime = MIMES[path.extname(file).toLowerCase()];
|
|
57
|
+
if (!mime) throw new Error(`--ref ${file}: must be a .png/.jpg/.jpeg/.webp file`);
|
|
58
|
+
let buf;
|
|
59
|
+
try {
|
|
60
|
+
buf = fs.readFileSync(file);
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Error(`--ref ${file}: can't read file`);
|
|
63
|
+
}
|
|
64
|
+
if (buf.length > 6e6) throw new Error(`--ref ${file}: over 6 MB — resize it first`);
|
|
65
|
+
return `data:${mime};base64,${buf.toString("base64")}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// "A hero shot of a grain bowl!" → "a-hero-shot-of-a-grain"
|
|
69
|
+
const promptSlug = (p) =>
|
|
70
|
+
p.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").split("-").slice(0, 5).join("-") || "image";
|
|
71
|
+
|
|
72
|
+
// Unique output path: <base>.<ext>, <base>-2.<ext>, ... (never overwrite silently).
|
|
73
|
+
function outPath(base, ext, taken) {
|
|
74
|
+
for (let n = 1; ; n++) {
|
|
75
|
+
const p = n === 1 ? `${base}.${ext}` : `${base}-${n}.${ext}`;
|
|
76
|
+
if (!taken.has(p) && !fs.existsSync(p)) {
|
|
77
|
+
taken.add(p);
|
|
78
|
+
return p;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const money = (v) => `$${(Number(v) || 0).toFixed(v >= 0.1 ? 2 : 4)}`;
|
|
84
|
+
|
|
85
|
+
async function cmdImageUsage(jsonOut) {
|
|
86
|
+
const session = await ensureSession();
|
|
87
|
+
const u = await apiGet("/v1/imagegen/usage", session);
|
|
88
|
+
if (jsonOut) {
|
|
89
|
+
process.stdout.write("\n" + JSON.stringify(u) + "\n"); // own line — machine callers parse the last non-empty line
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const cur = u.months[u.month] || { images: 0, cost: 0, byModel: {} };
|
|
93
|
+
log(`${c.b(u.email)} — image generation for ${c.b(u.month)}`);
|
|
94
|
+
log(` ${cur.images}/${u.limits.monthlyImages || "∞"} images · ${money(cur.cost)}/$${u.limits.monthlyCostUsd || "∞"}`);
|
|
95
|
+
for (const [m, v] of Object.entries(cur.byModel || {})) log(c.dim(` ${m}: ${v.images} images · ${money(v.cost)}`));
|
|
96
|
+
log(c.dim(` org this month: ${u.orgMonth.images} images · ${money(u.orgMonth.cost)} (budget $${u.limits.globalMonthlyCostUsd || "∞"})`));
|
|
97
|
+
const past = Object.keys(u.months).filter((m) => m !== u.month).sort().reverse();
|
|
98
|
+
for (const m of past.slice(0, 6)) log(c.dim(` ${m}: ${u.months[m].images} images · ${money(u.months[m].cost)}`));
|
|
99
|
+
if (!u.configured) log(c.dim(" (generation currently disabled on the broker — no OPENROUTER_API_KEY)"));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function cmdImage(args) {
|
|
103
|
+
const jsonOut = args.includes("--json");
|
|
104
|
+
if (args[0] === "usage") return cmdImageUsage(jsonOut);
|
|
105
|
+
|
|
106
|
+
// First positional = the prompt. Walk the args so a space-separated flag value
|
|
107
|
+
// (`--ref img.png`) is never mistaken for it.
|
|
108
|
+
const VALUE_FLAGS = new Set(["--out", "--model", "--ref"]);
|
|
109
|
+
let prompt;
|
|
110
|
+
for (let i = 0; i < args.length; i++) {
|
|
111
|
+
if (VALUE_FLAGS.has(args[i])) i++; // skip the flag's value
|
|
112
|
+
else if (!args[i].startsWith("--")) { prompt = args[i]; break; }
|
|
113
|
+
}
|
|
114
|
+
if (!prompt) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
'usage: calo-design image "<prompt>" [--out file.png] [--ref img.png]... [--model id] [--json]\n or: calo-design image usage'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const refImages = flagAll(args, "ref").map(refToDataUrl);
|
|
120
|
+
const model = flag(args, "model");
|
|
121
|
+
const out = flag(args, "out");
|
|
122
|
+
|
|
123
|
+
const session = await ensureSession();
|
|
124
|
+
if (!jsonOut) log(c.dim(`Generating${model ? ` with ${model}` : ""}… (typically 10–30s)`));
|
|
125
|
+
let res;
|
|
126
|
+
try {
|
|
127
|
+
res = await fetch(BROKER + "/v1/imagegen", {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${session}` },
|
|
130
|
+
body: JSON.stringify({ prompt, ...(model ? { model } : {}), ...(refImages.length ? { refImages } : {}) }),
|
|
131
|
+
signal: AbortSignal.timeout(180000),
|
|
132
|
+
});
|
|
133
|
+
} catch (e) {
|
|
134
|
+
throw new Error(`can't reach the Calo broker at ${BROKER} (${e.message})`);
|
|
135
|
+
}
|
|
136
|
+
const data = await res.json().catch(() => ({}));
|
|
137
|
+
if (!res.ok) throw new Error(data.error || `imagegen → HTTP ${res.status}`);
|
|
138
|
+
|
|
139
|
+
// Write files. --out names the first image exactly (extension from the actual
|
|
140
|
+
// mime wins over a mismatched one); extras and the no---out case get slug names.
|
|
141
|
+
const taken = new Set();
|
|
142
|
+
const files = [];
|
|
143
|
+
for (const [i, img] of data.images.entries()) {
|
|
144
|
+
const ext = EXTS[img.mime] || "png";
|
|
145
|
+
let file;
|
|
146
|
+
if (out && i === 0) {
|
|
147
|
+
const wanted = out.replace(/\.(png|jpe?g|webp)$/i, "");
|
|
148
|
+
file = outPath(wanted, ext, taken);
|
|
149
|
+
} else {
|
|
150
|
+
file = outPath(path.join(process.cwd(), promptSlug(prompt)), ext, taken);
|
|
151
|
+
}
|
|
152
|
+
fs.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
|
|
153
|
+
fs.writeFileSync(file, Buffer.from(img.b64, "base64"));
|
|
154
|
+
files.push({ file: path.resolve(file), url: img.url, mime: img.mime });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (jsonOut) {
|
|
158
|
+
const { images: _images, ...rest } = data; // drop the base64 payload from machine output
|
|
159
|
+
process.stdout.write("\n" + JSON.stringify({ ...rest, files }) + "\n");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
for (const f of files) {
|
|
163
|
+
log(`${c.g("✓")} ${f.file}`);
|
|
164
|
+
if (f.url) log(c.dim(` hosted: ${f.url}`));
|
|
165
|
+
}
|
|
166
|
+
if (data.text) log(c.dim(` model note: ${data.text}`));
|
|
167
|
+
const u = data.usage;
|
|
168
|
+
log(c.dim(` ${data.model} · this call ${money(data.cost)} · ${u.month}: ${u.images}/${u.limits.monthlyImages || "∞"} images, ${money(u.cost)}/$${u.limits.monthlyCostUsd || "∞"}`));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { cmdImage };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@calo-design/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "One-line setup for Calo design tooling: logs in with your Calo email and installs the calo-design skill + design-system packages. No GitHub account needed.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"calo-design": "bin/cli.js"
|