@zalify/cli 0.4.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/dist/cli.js +215 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11293,7 +11293,7 @@ function updateNotifier(options) {
|
|
|
11293
11293
|
|
|
11294
11294
|
// src/cli.ts
|
|
11295
11295
|
import { createRequire as createRequire2 } from "node:module";
|
|
11296
|
-
import { dirname, join as
|
|
11296
|
+
import { dirname, join as join4 } from "node:path";
|
|
11297
11297
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
11298
11298
|
|
|
11299
11299
|
// src/auth.ts
|
|
@@ -11847,10 +11847,219 @@ async function workspaceSet(slugOrId) {
|
|
|
11847
11847
|
console.log(`✓ Active workspace: ${found[1].name} (${found[1].slug})`);
|
|
11848
11848
|
}
|
|
11849
11849
|
|
|
11850
|
+
// src/images.ts
|
|
11851
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
11852
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, existsSync as existsSync3, rmSync as rmSync2 } from "node:fs";
|
|
11853
|
+
import { join as join3, resolve as resolve2 } from "node:path";
|
|
11854
|
+
var MAX_JOBS_PER_REQUEST = 40;
|
|
11855
|
+
var POLL_INTERVAL_MS = 5000;
|
|
11856
|
+
var POLL_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11857
|
+
var STREAM_BATCH = 20;
|
|
11858
|
+
function buildPrompt(manifest, entry) {
|
|
11859
|
+
const style = entry.type === "model" ? manifest.modelStyle ?? manifest.style : manifest.style;
|
|
11860
|
+
return `${style}
|
|
11861
|
+
|
|
11862
|
+
${entry.prompt}`;
|
|
11863
|
+
}
|
|
11864
|
+
function apiError(auth, label, status, json) {
|
|
11865
|
+
if (json.code === "UPGRADE_REQUIRED") {
|
|
11866
|
+
return new Error(`${json.error ?? "Paid plan required."}
|
|
11867
|
+
Upgrade: ${auth.appUrl}/store/${auth.workspaceSlug}/settings/billing`);
|
|
11868
|
+
}
|
|
11869
|
+
return new Error(`${label} ${status}: ${JSON.stringify(json)}`);
|
|
11870
|
+
}
|
|
11871
|
+
function sleep(ms) {
|
|
11872
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
11873
|
+
}
|
|
11874
|
+
async function saveProduced(imagesDir, indexPath, index, file2, asset) {
|
|
11875
|
+
const img = await fetch(asset.url);
|
|
11876
|
+
if (!img.ok) {
|
|
11877
|
+
console.error(` ✗ ${file2}: download ${img.status}`);
|
|
11878
|
+
return false;
|
|
11879
|
+
}
|
|
11880
|
+
const bytes = Buffer.from(await img.arrayBuffer());
|
|
11881
|
+
writeFileSync4(join3(imagesDir, file2), bytes);
|
|
11882
|
+
index[file2] = {
|
|
11883
|
+
assetId: asset.id,
|
|
11884
|
+
url: asset.url,
|
|
11885
|
+
checksum: createHash2("sha256").update(bytes).digest("hex")
|
|
11886
|
+
};
|
|
11887
|
+
writeFileSync4(indexPath, JSON.stringify(index, null, 2) + `
|
|
11888
|
+
`);
|
|
11889
|
+
return true;
|
|
11890
|
+
}
|
|
11891
|
+
async function submitJob(auth, manifest, entries) {
|
|
11892
|
+
const res = await fetch(`${auth.appUrl}/api/cli/images/jobs`, {
|
|
11893
|
+
method: "POST",
|
|
11894
|
+
headers: {
|
|
11895
|
+
"Content-Type": "application/json",
|
|
11896
|
+
Authorization: `Bearer ${auth.key}`
|
|
11897
|
+
},
|
|
11898
|
+
body: JSON.stringify({
|
|
11899
|
+
workspaceId: auth.workspaceId,
|
|
11900
|
+
jobs: entries.map((e) => ({
|
|
11901
|
+
prompt: buildPrompt(manifest, e),
|
|
11902
|
+
size: "1024x1024",
|
|
11903
|
+
count: 1
|
|
11904
|
+
}))
|
|
11905
|
+
})
|
|
11906
|
+
});
|
|
11907
|
+
if (res.status === 404)
|
|
11908
|
+
return null;
|
|
11909
|
+
const json = await res.json().catch(() => ({}));
|
|
11910
|
+
if (!res.ok)
|
|
11911
|
+
throw apiError(auth, "images/jobs", res.status, json);
|
|
11912
|
+
if (!json.jobId)
|
|
11913
|
+
throw new Error(`images/jobs: no jobId in response`);
|
|
11914
|
+
return json.jobId;
|
|
11915
|
+
}
|
|
11916
|
+
async function pollJob(auth, jobId, files, imagesDir, indexPath, index, counters) {
|
|
11917
|
+
const handled = new Set;
|
|
11918
|
+
const reported = new Set;
|
|
11919
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
11920
|
+
while (true) {
|
|
11921
|
+
const res = await fetch(`${auth.appUrl}/api/cli/images/jobs/${jobId}?workspaceId=${encodeURIComponent(auth.workspaceId)}`, { headers: { Authorization: `Bearer ${auth.key}` } });
|
|
11922
|
+
const json = await res.json().catch(() => ({}));
|
|
11923
|
+
if (!res.ok)
|
|
11924
|
+
throw apiError(auth, `images/jobs/${jobId}`, res.status, json);
|
|
11925
|
+
const total = json.total || files.length;
|
|
11926
|
+
for (const item of json.produced ?? []) {
|
|
11927
|
+
if (handled.has(item.jobIndex))
|
|
11928
|
+
continue;
|
|
11929
|
+
handled.add(item.jobIndex);
|
|
11930
|
+
const file2 = files[item.jobIndex];
|
|
11931
|
+
if (!file2)
|
|
11932
|
+
continue;
|
|
11933
|
+
if (await saveProduced(imagesDir, indexPath, index, file2, item.asset)) {
|
|
11934
|
+
counters.produced++;
|
|
11935
|
+
console.log(` ✓ ${file2} (${handled.size + reported.size}/${total})`);
|
|
11936
|
+
} else {
|
|
11937
|
+
counters.failed++;
|
|
11938
|
+
}
|
|
11939
|
+
}
|
|
11940
|
+
for (const f of json.failed ?? []) {
|
|
11941
|
+
if (reported.has(f.jobIndex))
|
|
11942
|
+
continue;
|
|
11943
|
+
reported.add(f.jobIndex);
|
|
11944
|
+
counters.failed++;
|
|
11945
|
+
console.error(` ✗ ${files[f.jobIndex] ?? `job ${f.jobIndex}`}: ${f.message}`);
|
|
11946
|
+
}
|
|
11947
|
+
if (json.status === "done")
|
|
11948
|
+
return;
|
|
11949
|
+
if (Date.now() >= deadline) {
|
|
11950
|
+
throw new Error(`Timed out after 30 minutes waiting for job ${jobId}. ` + `The job keeps running server-side — re-run \`zalify images generate\` to resume.`);
|
|
11951
|
+
}
|
|
11952
|
+
await sleep(POLL_INTERVAL_MS);
|
|
11953
|
+
}
|
|
11954
|
+
}
|
|
11955
|
+
async function streamingGenerate(auth, manifest, missing, imagesDir, indexPath, index, counters) {
|
|
11956
|
+
for (let i = 0;i < missing.length; i += STREAM_BATCH) {
|
|
11957
|
+
const batch = missing.slice(i, i + STREAM_BATCH);
|
|
11958
|
+
const res = await fetch(`${auth.appUrl}/api/assets/generate`, {
|
|
11959
|
+
method: "POST",
|
|
11960
|
+
headers: {
|
|
11961
|
+
"Content-Type": "application/json",
|
|
11962
|
+
Authorization: `Bearer ${auth.key}`
|
|
11963
|
+
},
|
|
11964
|
+
body: JSON.stringify({
|
|
11965
|
+
workspaceId: auth.workspaceId,
|
|
11966
|
+
jobs: batch.map((e) => ({
|
|
11967
|
+
prompt: buildPrompt(manifest, e),
|
|
11968
|
+
count: 1,
|
|
11969
|
+
size: "1024x1024"
|
|
11970
|
+
}))
|
|
11971
|
+
})
|
|
11972
|
+
});
|
|
11973
|
+
if (!res.ok || !res.body) {
|
|
11974
|
+
const json = await res.json().catch(() => ({}));
|
|
11975
|
+
throw apiError(auth, "generate", res.status, json);
|
|
11976
|
+
}
|
|
11977
|
+
const reader = res.body.getReader();
|
|
11978
|
+
const decoder = new TextDecoder;
|
|
11979
|
+
let buffer = "";
|
|
11980
|
+
while (true) {
|
|
11981
|
+
const { done, value } = await reader.read();
|
|
11982
|
+
if (done)
|
|
11983
|
+
break;
|
|
11984
|
+
buffer += decoder.decode(value, { stream: true });
|
|
11985
|
+
const lines = buffer.split(`
|
|
11986
|
+
`);
|
|
11987
|
+
buffer = lines.pop() ?? "";
|
|
11988
|
+
for (const line of lines) {
|
|
11989
|
+
if (!line.trim())
|
|
11990
|
+
continue;
|
|
11991
|
+
let event;
|
|
11992
|
+
try {
|
|
11993
|
+
event = JSON.parse(line);
|
|
11994
|
+
} catch {
|
|
11995
|
+
continue;
|
|
11996
|
+
}
|
|
11997
|
+
if (event.type === "image") {
|
|
11998
|
+
const entry = batch[event.jobIndex];
|
|
11999
|
+
if (!entry)
|
|
12000
|
+
continue;
|
|
12001
|
+
if (await saveProduced(imagesDir, indexPath, index, entry.file, event.asset)) {
|
|
12002
|
+
counters.produced++;
|
|
12003
|
+
console.log(` ✓ ${entry.file} (${counters.produced + counters.failed}/${missing.length})`);
|
|
12004
|
+
} else {
|
|
12005
|
+
counters.failed++;
|
|
12006
|
+
}
|
|
12007
|
+
} else if (event.type === "error") {
|
|
12008
|
+
const entry = batch[event.jobIndex];
|
|
12009
|
+
counters.failed++;
|
|
12010
|
+
console.error(` ✗ ${entry?.file ?? `job ${event.jobIndex}`}: ${event.message}`);
|
|
12011
|
+
}
|
|
12012
|
+
}
|
|
12013
|
+
}
|
|
12014
|
+
}
|
|
12015
|
+
}
|
|
12016
|
+
async function imagesGenerate(storeDir) {
|
|
12017
|
+
const auth = requireActive();
|
|
12018
|
+
const imagesDir = join3(resolve2(storeDir), "images");
|
|
12019
|
+
const manifestPath = join3(imagesDir, "manifest.json");
|
|
12020
|
+
if (!existsSync3(manifestPath)) {
|
|
12021
|
+
throw new Error(`No images/manifest.json in ${storeDir}`);
|
|
12022
|
+
}
|
|
12023
|
+
const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
|
|
12024
|
+
const indexPath = join3(imagesDir, "assets.json");
|
|
12025
|
+
const index = existsSync3(indexPath) ? JSON.parse(readFileSync3(indexPath, "utf8")) : {};
|
|
12026
|
+
const jobStatePath = join3(imagesDir, ".imagegen-job.json");
|
|
12027
|
+
const counters = { produced: 0, failed: 0 };
|
|
12028
|
+
if (existsSync3(jobStatePath)) {
|
|
12029
|
+
const state = JSON.parse(readFileSync3(jobStatePath, "utf8"));
|
|
12030
|
+
console.log(`Resuming job ${state.jobId}`);
|
|
12031
|
+
await pollJob(auth, state.jobId, state.files, imagesDir, indexPath, index, counters);
|
|
12032
|
+
rmSync2(jobStatePath, { force: true });
|
|
12033
|
+
console.log(`Done: ${counters.produced} generated (already in the asset library), ${counters.failed} failed${counters.failed ? " — re-run to retry" : ""}.`);
|
|
12034
|
+
return;
|
|
12035
|
+
}
|
|
12036
|
+
const missing = manifest.images.filter((e) => !existsSync3(join3(imagesDir, e.file)));
|
|
12037
|
+
if (missing.length === 0) {
|
|
12038
|
+
console.log("All manifest images already exist locally.");
|
|
12039
|
+
return;
|
|
12040
|
+
}
|
|
12041
|
+
console.log(`Generating ${missing.length} image(s) on Zalify (workspace "${auth.workspaceName}")…`);
|
|
12042
|
+
for (let i = 0;i < missing.length; i += MAX_JOBS_PER_REQUEST) {
|
|
12043
|
+
const chunk = missing.slice(i, i + MAX_JOBS_PER_REQUEST);
|
|
12044
|
+
const jobId = await submitJob(auth, manifest, chunk);
|
|
12045
|
+
if (jobId === null) {
|
|
12046
|
+
console.log("job API unavailable — using streaming mode");
|
|
12047
|
+
await streamingGenerate(auth, manifest, missing.slice(i), imagesDir, indexPath, index, counters);
|
|
12048
|
+
break;
|
|
12049
|
+
}
|
|
12050
|
+
const state = { jobId, files: chunk.map((e) => e.file) };
|
|
12051
|
+
writeFileSync4(jobStatePath, JSON.stringify(state, null, 2) + `
|
|
12052
|
+
`);
|
|
12053
|
+
await pollJob(auth, jobId, state.files, imagesDir, indexPath, index, counters);
|
|
12054
|
+
rmSync2(jobStatePath, { force: true });
|
|
12055
|
+
}
|
|
12056
|
+
console.log(`Done: ${counters.produced} generated (already in the asset library), ${counters.failed} failed${counters.failed ? " — re-run to retry" : ""}.`);
|
|
12057
|
+
}
|
|
12058
|
+
|
|
11850
12059
|
// src/cli.ts
|
|
11851
12060
|
var __dirname4 = dirname(fileURLToPath3(import.meta.url));
|
|
11852
12061
|
var require2 = createRequire2(import.meta.url);
|
|
11853
|
-
var pkg = require2(
|
|
12062
|
+
var pkg = require2(join4(__dirname4, "..", "package.json"));
|
|
11854
12063
|
try {
|
|
11855
12064
|
updateNotifier({ pkg }).notify({
|
|
11856
12065
|
isGlobal: true,
|
|
@@ -11894,6 +12103,10 @@ workspace.command("list", { isDefault: true }).description("List authorized work
|
|
|
11894
12103
|
workspace.command("set <slug-or-id>").description("Switch the active workspace (local, no re-login)").action(async (slugOrId) => {
|
|
11895
12104
|
await workspaceSet(slugOrId);
|
|
11896
12105
|
});
|
|
12106
|
+
var images = program2.command("images").description("Generate images on Zalify infrastructure");
|
|
12107
|
+
images.command("generate <store-dir>").description("Generate missing manifest images server-side (results land in the asset library and are downloaded locally)").action(async (storeDir) => {
|
|
12108
|
+
await imagesGenerate(storeDir);
|
|
12109
|
+
});
|
|
11897
12110
|
var assets = program2.command("assets").description("Sync images with the Zalify asset library");
|
|
11898
12111
|
assets.command("push <store-dir>").description("Upload <store-dir>/images/*.png (sha256 dedup, writes assets.json)").action(async (storeDir) => {
|
|
11899
12112
|
await assetsPush(storeDir);
|