pixelkiln 0.12.0 → 0.13.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/CONTRIBUTING.md +6 -0
- package/PROVIDERS.md +45 -69
- package/README.md +9 -8
- package/SECURITY.md +10 -1
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +524 -145
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +237 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +75 -11
- package/dist/index.d.ts +75 -11
- package/dist/index.js +235 -56
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +8 -7
- package/docs/ARCHITECTURE.md +15 -4
- package/docs/CLI.md +32 -18
- package/docs/COMFYUI.md +3 -1
- package/docs/GETTING_STARTED.md +12 -5
- package/docs/LIBRARY.md +28 -11
- package/docs/MANIFEST.md +11 -7
- package/docs/MIXED_PROVIDERS.md +134 -0
- package/docs/PIXELLAB.md +3 -0
- package/docs/QUALITY.md +3 -0
- package/docs/README.md +1 -0
- package/docs/RECIPES.md +1 -1
- package/docs/RECOVERY.md +23 -8
- package/docs/RETRO_DIFFUSION.md +10 -2
- package/package.json +2 -1
- package/schema/manifest.schema.json +4 -0
- package/schema/recipe.schema.json +4 -0
- package/skills/pixelkiln/SKILL.md +8 -3
- package/skills/pixelkiln/references/mixed-providers.md +25 -32
- package/skills/pixelkiln/references/retro-diffusion.md +4 -0
package/dist/cli.js
CHANGED
|
@@ -1403,6 +1403,8 @@ var StyleImageSchema = z2.object({
|
|
|
1403
1403
|
path: z2.string()
|
|
1404
1404
|
});
|
|
1405
1405
|
var StyleSchema = z2.object({
|
|
1406
|
+
/** Generation backend for this style. Omit to inherit the manifest default. */
|
|
1407
|
+
provider: z2.string().min(1).optional(),
|
|
1406
1408
|
// `map` is the default because it is 20-40x cheaper and correct for any
|
|
1407
1409
|
// asset that is not going to be rotated or animated.
|
|
1408
1410
|
generator: GeneratorSchema.default("map"),
|
|
@@ -1963,6 +1965,7 @@ function firstUrl(urls) {
|
|
|
1963
1965
|
|
|
1964
1966
|
// src/providers/retrodiffusion.ts
|
|
1965
1967
|
var DEFAULT_BASE_URL2 = "https://api.retrodiffusion.ai/v1";
|
|
1968
|
+
var SOURCE_PROTOCOL = "retrodiffusion:";
|
|
1966
1969
|
var RetroDiffusionClient = class {
|
|
1967
1970
|
constructor(token, baseUrl = DEFAULT_BASE_URL2, request = fetch) {
|
|
1968
1971
|
this.token = token;
|
|
@@ -2042,7 +2045,7 @@ var RetroDiffusionProvider = class _RetroDiffusionProvider {
|
|
|
2042
2045
|
return new _RetroDiffusionProvider(new RetroDiffusionClient(void 0));
|
|
2043
2046
|
}
|
|
2044
2047
|
static forDownloads() {
|
|
2045
|
-
return _RetroDiffusionProvider.
|
|
2048
|
+
return new _RetroDiffusionProvider(new RetroDiffusionClient(process.env.RD_API_KEY));
|
|
2046
2049
|
}
|
|
2047
2050
|
supports(generator) {
|
|
2048
2051
|
return generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "animation";
|
|
@@ -2148,11 +2151,15 @@ var RetroDiffusionProvider = class _RetroDiffusionProvider {
|
|
|
2148
2151
|
const urls = sources.map((source) => source.url);
|
|
2149
2152
|
if (!urls.length) return { status: "failed", error: "Retro Diffusion task returned no images" };
|
|
2150
2153
|
if (urls.length > 1) return { status: "review", candidateUrls: urls };
|
|
2154
|
+
const durableSources = sources.map((source, index) => ({
|
|
2155
|
+
...source,
|
|
2156
|
+
url: sourceRef2(jobId, index, mediaType)
|
|
2157
|
+
}));
|
|
2151
2158
|
return {
|
|
2152
2159
|
status: "ready",
|
|
2153
2160
|
objectId: `${jobId}#0`,
|
|
2154
|
-
sourceUrl:
|
|
2155
|
-
sources,
|
|
2161
|
+
sourceUrl: durableSources[0].url,
|
|
2162
|
+
sources: durableSources,
|
|
2156
2163
|
metadata: {
|
|
2157
2164
|
balanceCost: task.result?.balance_cost ?? null,
|
|
2158
2165
|
remainingBalance: task.result?.remaining_balance ?? null,
|
|
@@ -2171,9 +2178,26 @@ var RetroDiffusionProvider = class _RetroDiffusionProvider {
|
|
|
2171
2178
|
if (task.status !== "succeeded") throw new Error(`Retro Diffusion task ${jobId} is not ready`);
|
|
2172
2179
|
const url = resultSources(task.result, MediaType.PNG)[index]?.url;
|
|
2173
2180
|
if (!url) throw new Error(`Retro Diffusion task ${jobId} has no candidate at index ${index}`);
|
|
2174
|
-
return { objectId: `${jobId}#${index}`, sourceUrl:
|
|
2181
|
+
return { objectId: `${jobId}#${index}`, sourceUrl: sourceRef2(jobId, index, MediaType.PNG) };
|
|
2175
2182
|
}
|
|
2176
2183
|
async download(url) {
|
|
2184
|
+
if (url.startsWith(`${SOURCE_PROTOCOL}//`)) {
|
|
2185
|
+
const reference = parseSourceRef(url);
|
|
2186
|
+
const task = await this.client.task(reference.jobId);
|
|
2187
|
+
if (task.status !== "succeeded") {
|
|
2188
|
+
throw new Error(`Retro Diffusion task ${reference.jobId} is not ready`);
|
|
2189
|
+
}
|
|
2190
|
+
const source = resultSources(task.result, reference.mediaType)[reference.index]?.url;
|
|
2191
|
+
if (!source) {
|
|
2192
|
+
throw new Error(
|
|
2193
|
+
`Retro Diffusion task ${reference.jobId} has no output at index ${reference.index}`
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
return this.downloadResolved(source);
|
|
2197
|
+
}
|
|
2198
|
+
return this.downloadResolved(url);
|
|
2199
|
+
}
|
|
2200
|
+
async downloadResolved(url) {
|
|
2177
2201
|
const data = /^data:[^;]+;base64,(.+)$/.exec(url)?.[1];
|
|
2178
2202
|
if (data) return Buffer.from(data, "base64");
|
|
2179
2203
|
const response = await fetch(url);
|
|
@@ -2184,6 +2208,23 @@ var RetroDiffusionProvider = class _RetroDiffusionProvider {
|
|
|
2184
2208
|
return { unit: "usd", remaining: await this.client.balance() };
|
|
2185
2209
|
}
|
|
2186
2210
|
};
|
|
2211
|
+
function sourceRef2(jobId, index, mediaType) {
|
|
2212
|
+
const url = new URL("retrodiffusion://output");
|
|
2213
|
+
url.searchParams.set("job", jobId);
|
|
2214
|
+
url.searchParams.set("index", String(index));
|
|
2215
|
+
url.searchParams.set("mediaType", mediaType);
|
|
2216
|
+
return url.href;
|
|
2217
|
+
}
|
|
2218
|
+
function parseSourceRef(value) {
|
|
2219
|
+
const url = new URL(value);
|
|
2220
|
+
const jobId = url.searchParams.get("job");
|
|
2221
|
+
const index = Number(url.searchParams.get("index"));
|
|
2222
|
+
const rawMediaType = url.searchParams.get("mediaType");
|
|
2223
|
+
if (url.protocol !== SOURCE_PROTOCOL || url.hostname !== "output" || !jobId || !Number.isInteger(index) || index < 0 || rawMediaType !== MediaType.PNG && rawMediaType !== MediaType.GIF) {
|
|
2224
|
+
throw new Error("Invalid durable Retro Diffusion source reference");
|
|
2225
|
+
}
|
|
2226
|
+
return { jobId, index, mediaType: rawMediaType };
|
|
2227
|
+
}
|
|
2187
2228
|
function retroOptions(spec) {
|
|
2188
2229
|
return spec.providerOptions;
|
|
2189
2230
|
}
|
|
@@ -2353,7 +2394,17 @@ ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
|
2353
2394
|
}
|
|
2354
2395
|
async function resolveSpecs(loaded, filter) {
|
|
2355
2396
|
const { manifest, root } = loaded;
|
|
2356
|
-
const
|
|
2397
|
+
const providerOverride = filter?.provider;
|
|
2398
|
+
const providers = /* @__PURE__ */ new Map();
|
|
2399
|
+
const providerFor = (id) => {
|
|
2400
|
+
if (providerOverride) return providerOverride;
|
|
2401
|
+
let provider = providers.get(id);
|
|
2402
|
+
if (!provider) {
|
|
2403
|
+
provider = createProvider(id, "offline");
|
|
2404
|
+
providers.set(id, provider);
|
|
2405
|
+
}
|
|
2406
|
+
return provider;
|
|
2407
|
+
};
|
|
2357
2408
|
const specs = [];
|
|
2358
2409
|
const styleIds = Object.keys(manifest.styles).filter(
|
|
2359
2410
|
(id) => !filter?.styles?.length || filter.styles.includes(id)
|
|
@@ -2389,6 +2440,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
2389
2440
|
}
|
|
2390
2441
|
for (const styleId of styleIds) {
|
|
2391
2442
|
const style = manifest.styles[styleId];
|
|
2443
|
+
const activeProvider = providerFor(style.provider ?? manifest.provider);
|
|
2392
2444
|
const rawProviderOptions = style.providerOptions[activeProvider.id] ?? {};
|
|
2393
2445
|
const optionResolution = activeProvider.resolveOptions ? await activeProvider.resolveOptions(rawProviderOptions, { root, styleId }) : { options: rawProviderOptions };
|
|
2394
2446
|
const providerOptions = optionResolution.options;
|
|
@@ -3132,13 +3184,39 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
3132
3184
|
items.push({ spec, key, state, reason });
|
|
3133
3185
|
}
|
|
3134
3186
|
const actionable = items.filter((i) => i.state === "missing" || i.state === "stale" || i.state === "failed");
|
|
3135
|
-
const
|
|
3136
|
-
|
|
3187
|
+
const groupsByProvider = /* @__PURE__ */ new Map();
|
|
3188
|
+
for (const item of actionable) {
|
|
3189
|
+
const provider = item.spec.provider;
|
|
3190
|
+
const costUnit = item.spec.costUnit ?? "generations";
|
|
3191
|
+
const group = groupsByProvider.get(provider);
|
|
3192
|
+
if (group && group.costUnit !== costUnit) {
|
|
3193
|
+
throw new Error(
|
|
3194
|
+
`Provider "${provider}" returned mixed cost units (${group.costUnit}, ${costUnit})`
|
|
3195
|
+
);
|
|
3196
|
+
}
|
|
3197
|
+
if (group) {
|
|
3198
|
+
group.actionable.push(item);
|
|
3199
|
+
group.cost += item.spec.cost;
|
|
3200
|
+
group.candidates += item.spec.candidates;
|
|
3201
|
+
} else {
|
|
3202
|
+
groupsByProvider.set(provider, {
|
|
3203
|
+
provider,
|
|
3204
|
+
costUnit,
|
|
3205
|
+
cost: item.spec.cost,
|
|
3206
|
+
candidates: item.spec.candidates,
|
|
3207
|
+
actionable: [item]
|
|
3208
|
+
});
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
const groups = [...groupsByProvider.values()].sort((a, b) => a.provider.localeCompare(b.provider));
|
|
3212
|
+
const fallbackUnits = new Set(specs.map((spec) => spec.costUnit));
|
|
3213
|
+
const single = groups.length === 1 ? groups[0] : null;
|
|
3137
3214
|
return {
|
|
3138
3215
|
items,
|
|
3139
3216
|
actionable,
|
|
3140
|
-
|
|
3141
|
-
|
|
3217
|
+
groups,
|
|
3218
|
+
cost: single?.cost ?? (groups.length ? null : 0),
|
|
3219
|
+
costUnit: single?.costUnit ?? (groups.length || fallbackUnits.size > 1 ? null : fallbackUnits.values().next().value ?? (specs.length ? "generations" : "free")),
|
|
3142
3220
|
candidates: actionable.reduce((sum, i) => sum + i.spec.candidates, 0)
|
|
3143
3221
|
};
|
|
3144
3222
|
}
|
|
@@ -3245,6 +3323,10 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
3245
3323
|
submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3246
3324
|
cost: estimate.amount,
|
|
3247
3325
|
costUnit: estimate.unit,
|
|
3326
|
+
// Persist routing before the request starts. If the process stops while
|
|
3327
|
+
// the provider is accepting work, a resumed run must not fall back to
|
|
3328
|
+
// PixelLab (the legacy lock default) for another provider's entry.
|
|
3329
|
+
provider: provider.id,
|
|
3248
3330
|
downloadedAt: null
|
|
3249
3331
|
});
|
|
3250
3332
|
await saveLock(lockPath, lock);
|
|
@@ -3257,8 +3339,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
3257
3339
|
// A multi-candidate generator routes through review; record the parent
|
|
3258
3340
|
// so `pick` knows where to look.
|
|
3259
3341
|
reviewObjectId: estimate.candidates > 1 && !spec.tileFeature ? jobId : null,
|
|
3260
|
-
status: "processing"
|
|
3261
|
-
provider: provider.id
|
|
3342
|
+
status: "processing"
|
|
3262
3343
|
});
|
|
3263
3344
|
inFlight.set(jobId, spec);
|
|
3264
3345
|
submitted++;
|
|
@@ -3286,9 +3367,10 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
3286
3367
|
});
|
|
3287
3368
|
const started = Date.now();
|
|
3288
3369
|
const specByKey = new Map((opts.specs ?? []).map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
3370
|
+
const selectedKeys = opts.specs ? new Set(specByKey.keys()) : null;
|
|
3289
3371
|
const result = { review: 0, completed: 0, failed: 0, stillRunning: 0 };
|
|
3290
3372
|
const pending = () => Object.entries(lock.entries).filter(
|
|
3291
|
-
([, e]) => e.jobId && (e.status === "pending" || e.status === "processing")
|
|
3373
|
+
([key, e]) => e.provider === provider.id && (!selectedKeys || selectedKeys.has(key)) && e.jobId && (e.status === "pending" || e.status === "processing")
|
|
3292
3374
|
);
|
|
3293
3375
|
while (pending().length > 0) {
|
|
3294
3376
|
if (Date.now() - started > timeout) {
|
|
@@ -3343,6 +3425,45 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
3343
3425
|
import { existsSync as existsSync6 } from "fs";
|
|
3344
3426
|
import { mkdir as mkdir2, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
3345
3427
|
import path9 from "path";
|
|
3428
|
+
|
|
3429
|
+
// src/source-url.ts
|
|
3430
|
+
var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
3431
|
+
"access_token",
|
|
3432
|
+
"apikey",
|
|
3433
|
+
"api_key",
|
|
3434
|
+
"auth",
|
|
3435
|
+
"authorization",
|
|
3436
|
+
"awsaccesskeyid",
|
|
3437
|
+
"googleaccessid",
|
|
3438
|
+
"key",
|
|
3439
|
+
"sig"
|
|
3440
|
+
]);
|
|
3441
|
+
function isSensitiveSourceUrl(value) {
|
|
3442
|
+
let parsed;
|
|
3443
|
+
try {
|
|
3444
|
+
parsed = new URL(value);
|
|
3445
|
+
} catch {
|
|
3446
|
+
return false;
|
|
3447
|
+
}
|
|
3448
|
+
if (parsed.username || parsed.password) return true;
|
|
3449
|
+
for (const key of parsed.searchParams.keys()) {
|
|
3450
|
+
const normalized = key.toLowerCase();
|
|
3451
|
+
if (SENSITIVE_QUERY_KEYS.has(normalized) || normalized.includes("credential") || normalized.includes("signature") || normalized.includes("security-token") || normalized.includes("token")) return true;
|
|
3452
|
+
}
|
|
3453
|
+
return false;
|
|
3454
|
+
}
|
|
3455
|
+
function shouldPersistSourceUrl(value) {
|
|
3456
|
+
let parsed;
|
|
3457
|
+
try {
|
|
3458
|
+
parsed = new URL(value);
|
|
3459
|
+
} catch {
|
|
3460
|
+
return true;
|
|
3461
|
+
}
|
|
3462
|
+
if (parsed.protocol === "data:" || parsed.protocol === "file:") return false;
|
|
3463
|
+
return !isSensitiveSourceUrl(value);
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
// src/pipeline/fetch.ts
|
|
3346
3467
|
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
3347
3468
|
const log2 = opts.onProgress ?? (() => {
|
|
3348
3469
|
});
|
|
@@ -3351,6 +3472,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3351
3472
|
normalizeLockOutputPaths(lock, specs);
|
|
3352
3473
|
const cacheDir = opts.cacheDir === false ? null : path9.resolve(opts.cacheDir ?? path9.join(path9.dirname(lockPath), ".pixelkiln", "cache"));
|
|
3353
3474
|
const pending = Object.entries(lock.entries).filter(([key, e]) => {
|
|
3475
|
+
if (e.provider !== provider.id || !specByKey.has(key)) return false;
|
|
3354
3476
|
if (e.status === "selected" || e.status === "download-failed") return true;
|
|
3355
3477
|
if (!opts.repair || e.status !== "downloaded") return false;
|
|
3356
3478
|
const spec = specByKey.get(key);
|
|
@@ -3444,15 +3566,16 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3444
3566
|
await rename2(tmp, target);
|
|
3445
3567
|
log2(` wrote ${path9.relative(process.cwd(), target)}`);
|
|
3446
3568
|
}
|
|
3569
|
+
const persistentSources = sources.filter((source) => shouldPersistSourceUrl(source.url));
|
|
3447
3570
|
upsert(lock, key, {
|
|
3448
3571
|
status: "downloaded",
|
|
3449
3572
|
provider: provider.id,
|
|
3450
3573
|
outputs,
|
|
3451
|
-
//
|
|
3452
|
-
//
|
|
3453
|
-
//
|
|
3454
|
-
|
|
3455
|
-
|
|
3574
|
+
// A finished lock may be committed. Inline bytes, machine-local
|
|
3575
|
+
// paths, and credential-bearing signed URLs are useful while a
|
|
3576
|
+
// download is retryable but must not survive successful ingestion.
|
|
3577
|
+
sourceUrl: persistentSources[0]?.url ?? null,
|
|
3578
|
+
sourceUrls: persistentSources,
|
|
3456
3579
|
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3457
3580
|
error: null
|
|
3458
3581
|
});
|
|
@@ -3516,7 +3639,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
|
|
|
3516
3639
|
let tagged = 0;
|
|
3517
3640
|
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
3518
3641
|
const spec = specByKey.get(key);
|
|
3519
|
-
if (!spec || !entry.objectId) continue;
|
|
3642
|
+
if (!spec || entry.provider !== provider.id || !entry.objectId) continue;
|
|
3520
3643
|
if (entry.generator === "pixflux") continue;
|
|
3521
3644
|
if (!provider.setTags) return tagged;
|
|
3522
3645
|
try {
|
|
@@ -3622,30 +3745,51 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
3622
3745
|
unsettled ? "warning" : "ok",
|
|
3623
3746
|
unsettled ? `${unsettled} spec(s) are not current: ` + Object.entries(counts).filter(([, n]) => n > 0).map(([state, n]) => `${n} ${state}`).join(", ") : "every resolved spec is current"
|
|
3624
3747
|
);
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3748
|
+
const legacyTarget = !opts.providers;
|
|
3749
|
+
const targets = opts.providers ?? [{
|
|
3750
|
+
id: opts.provider?.id ?? "provider",
|
|
3751
|
+
provider: opts.provider,
|
|
3752
|
+
apiKeyPresent: opts.apiKeyPresent ?? Boolean(opts.provider),
|
|
3753
|
+
credentialEnv: opts.credentialEnv
|
|
3754
|
+
}];
|
|
3755
|
+
for (const target of targets) {
|
|
3756
|
+
const checkId = targets.length === 1 ? "provider" : `provider:${target.id}`;
|
|
3757
|
+
if (opts.offline) {
|
|
3758
|
+
add(checkId, "warning", `${target.id} connectivity skipped (--dry-run)`);
|
|
3759
|
+
} else if (!target.provider) {
|
|
3760
|
+
add(
|
|
3761
|
+
checkId,
|
|
3762
|
+
"error",
|
|
3763
|
+
!target.apiKeyPresent ? `${target.credentialEnv ?? (legacyTarget ? "PIXELLAB_API_KEY" : `${target.id} credential`)} is not configured` : `${target.id} is not configured`
|
|
3764
|
+
);
|
|
3765
|
+
} else if (!target.provider.balance && !target.provider.checkConnection) {
|
|
3766
|
+
add(checkId, "ok", `${target.id} configured; connectivity check unavailable`);
|
|
3767
|
+
} else if (!target.provider.balance) {
|
|
3768
|
+
try {
|
|
3769
|
+
await target.provider.checkConnection();
|
|
3770
|
+
add(checkId, "ok", `${target.id} reachable; balance reporting unavailable`);
|
|
3771
|
+
} catch (err) {
|
|
3772
|
+
add(
|
|
3773
|
+
checkId,
|
|
3774
|
+
"error",
|
|
3775
|
+
`${legacyTarget ? "provider" : target.id} connectivity failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3776
|
+
);
|
|
3777
|
+
}
|
|
3778
|
+
} else {
|
|
3779
|
+
try {
|
|
3780
|
+
const balance = await target.provider.balance();
|
|
3781
|
+
add(
|
|
3782
|
+
checkId,
|
|
3783
|
+
"ok",
|
|
3784
|
+
`${target.id} reachable; ${balance.remaining} ${balance.unit} remaining`
|
|
3785
|
+
);
|
|
3786
|
+
} catch (err) {
|
|
3787
|
+
add(
|
|
3788
|
+
checkId,
|
|
3789
|
+
"error",
|
|
3790
|
+
`${legacyTarget ? "provider" : target.id} connectivity failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3791
|
+
);
|
|
3792
|
+
}
|
|
3649
3793
|
}
|
|
3650
3794
|
}
|
|
3651
3795
|
return { ok: !checks.some((c) => c.level === "error"), checks };
|
|
@@ -3797,6 +3941,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3797
3941
|
ambiguous.push(`${key}: already mapped to ${existing.objectId}, also matches ${obj.id}`);
|
|
3798
3942
|
continue;
|
|
3799
3943
|
}
|
|
3944
|
+
const durableSource = shouldPersistSourceUrl(url) ? url : null;
|
|
3800
3945
|
upsert(lock, key, {
|
|
3801
3946
|
styleId: spec.styleId,
|
|
3802
3947
|
assetId: spec.assetId,
|
|
@@ -3813,7 +3958,8 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3813
3958
|
candidateIndex: null,
|
|
3814
3959
|
status: "downloaded",
|
|
3815
3960
|
error: null,
|
|
3816
|
-
sourceUrl:
|
|
3961
|
+
sourceUrl: durableSource,
|
|
3962
|
+
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
3817
3963
|
outputs: [{ path: portableOutputPath(spec.outFile, spec.root), sha256: hash }],
|
|
3818
3964
|
submittedAt: obj.createdAt,
|
|
3819
3965
|
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3863,9 +4009,12 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
|
|
|
3863
4009
|
async function writePromptsBack(manifestPath, lock, opts = {}) {
|
|
3864
4010
|
const log2 = opts.onProgress ?? (() => {
|
|
3865
4011
|
});
|
|
4012
|
+
const selectedAssets = opts.assetIds ? new Set(opts.assetIds) : null;
|
|
3866
4013
|
const raw = JSON.parse(await readFile6(manifestPath, "utf8"));
|
|
3867
4014
|
const best = /* @__PURE__ */ new Map();
|
|
3868
4015
|
for (const entry of Object.values(lock.entries)) {
|
|
4016
|
+
if (opts.provider && entry.provider !== opts.provider) continue;
|
|
4017
|
+
if (selectedAssets && !selectedAssets.has(entry.assetId)) continue;
|
|
3869
4018
|
if (!entry.objectId || !entry.prompt) continue;
|
|
3870
4019
|
const current = best.get(entry.assetId);
|
|
3871
4020
|
if (!current || entry.prompt.length > current.length) best.set(entry.assetId, entry.prompt);
|
|
@@ -3873,6 +4022,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
|
|
|
3873
4022
|
let filled = 0;
|
|
3874
4023
|
const stillEmpty = [];
|
|
3875
4024
|
for (const [id, asset] of Object.entries(raw.assets)) {
|
|
4025
|
+
if (selectedAssets && !selectedAssets.has(id)) continue;
|
|
3876
4026
|
if (asset.prompt && asset.prompt.trim()) continue;
|
|
3877
4027
|
const recovered = best.get(id);
|
|
3878
4028
|
if (recovered) {
|
|
@@ -4188,8 +4338,10 @@ function serveReviewPage(opts) {
|
|
|
4188
4338
|
async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
4189
4339
|
const log2 = opts.onProgress ?? (() => {
|
|
4190
4340
|
});
|
|
4341
|
+
const selectedKeys = opts.keys ? new Set(opts.keys) : null;
|
|
4191
4342
|
const groups = [];
|
|
4192
4343
|
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
4344
|
+
if (entry.provider !== provider.id || selectedKeys && !selectedKeys.has(key)) continue;
|
|
4193
4345
|
if (entry.status !== "review" || !entry.reviewObjectId) continue;
|
|
4194
4346
|
try {
|
|
4195
4347
|
const state = await provider.poll(entry.reviewObjectId, entry.generator);
|
|
@@ -4355,7 +4507,10 @@ async function writeManifestFile(target, manifest) {
|
|
|
4355
4507
|
import { readFile as readFile8 } from "fs/promises";
|
|
4356
4508
|
import { existsSync as existsSync11 } from "fs";
|
|
4357
4509
|
import path13 from "path";
|
|
4358
|
-
|
|
4510
|
+
function providerClaimId(provider, objectId) {
|
|
4511
|
+
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
4512
|
+
}
|
|
4513
|
+
async function loadClaims(lockPaths, opts = {}) {
|
|
4359
4514
|
const claimed = /* @__PURE__ */ new Set();
|
|
4360
4515
|
for (const p of lockPaths) {
|
|
4361
4516
|
if (!existsSync11(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
@@ -4366,9 +4521,13 @@ async function loadClaims(lockPaths) {
|
|
|
4366
4521
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
4367
4522
|
}
|
|
4368
4523
|
for (const entry of Object.values(parsed.entries)) {
|
|
4369
|
-
if (entry.
|
|
4370
|
-
|
|
4371
|
-
|
|
4524
|
+
if (opts.provider && entry.provider !== opts.provider) continue;
|
|
4525
|
+
const add = (id) => {
|
|
4526
|
+
if (id) claimed.add(opts.qualify ? providerClaimId(entry.provider, id) : id);
|
|
4527
|
+
};
|
|
4528
|
+
add(entry.objectId);
|
|
4529
|
+
add(entry.reviewObjectId);
|
|
4530
|
+
add(entry.jobId);
|
|
4372
4531
|
}
|
|
4373
4532
|
}
|
|
4374
4533
|
return claimed;
|
|
@@ -4379,7 +4538,9 @@ async function findOrphans(provider, claimed, opts = {}) {
|
|
|
4379
4538
|
const all = [];
|
|
4380
4539
|
for await (const obj of requireList(provider)()) all.push(obj);
|
|
4381
4540
|
log2(` scanned ${all.length} object(s) on the account`);
|
|
4382
|
-
const orphans = all.filter(
|
|
4541
|
+
const orphans = all.filter(
|
|
4542
|
+
(o) => !claimed.has(o.id) && !claimed.has(providerClaimId(provider.id, o.id)) && o.status === "completed" && o.previewUrl
|
|
4543
|
+
).map((o) => ({
|
|
4383
4544
|
id: o.id,
|
|
4384
4545
|
prompt: o.prompt || "(no prompt recorded)",
|
|
4385
4546
|
width: o.width,
|
|
@@ -4674,7 +4835,7 @@ async function workspaceClaims(ws, dir) {
|
|
|
4674
4835
|
lockPaths.push(lockPath);
|
|
4675
4836
|
let projectClaims;
|
|
4676
4837
|
try {
|
|
4677
|
-
projectClaims = await loadClaims([lockPath]);
|
|
4838
|
+
projectClaims = await loadClaims([lockPath], { qualify: true });
|
|
4678
4839
|
} catch (err) {
|
|
4679
4840
|
throw new Error(
|
|
4680
4841
|
`Project "${project.id}" lockfile is unreadable: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -4712,9 +4873,14 @@ async function workspaceStatus(ws, dir) {
|
|
|
4712
4873
|
lock: lockPath
|
|
4713
4874
|
};
|
|
4714
4875
|
try {
|
|
4715
|
-
const provider = createProvider(project.provider, "offline");
|
|
4716
4876
|
const loaded = await loadManifest(manifestPath);
|
|
4717
|
-
const specs = await resolveSpecs(loaded
|
|
4877
|
+
const specs = await resolveSpecs(loaded);
|
|
4878
|
+
const providers = [...new Set(
|
|
4879
|
+
Object.values(loaded.manifest.styles).map(
|
|
4880
|
+
(style) => style.provider ?? loaded.manifest.provider
|
|
4881
|
+
)
|
|
4882
|
+
)].sort();
|
|
4883
|
+
if (!providers.length) providers.push(loaded.manifest.provider);
|
|
4718
4884
|
const lock = await loadLock(lockPath);
|
|
4719
4885
|
normalizeLockOutputPaths(lock, specs);
|
|
4720
4886
|
const plan = await buildPlan(specs, lock);
|
|
@@ -4728,6 +4894,8 @@ async function workspaceStatus(ws, dir) {
|
|
|
4728
4894
|
}
|
|
4729
4895
|
projects.push({
|
|
4730
4896
|
...base,
|
|
4897
|
+
provider: providers.length === 1 ? providers[0] : project.provider,
|
|
4898
|
+
providers,
|
|
4731
4899
|
entries: Object.keys(lock.entries).length,
|
|
4732
4900
|
byState,
|
|
4733
4901
|
spendByUnit: spend,
|
|
@@ -4736,6 +4904,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
4736
4904
|
} catch (err) {
|
|
4737
4905
|
projects.push({
|
|
4738
4906
|
...base,
|
|
4907
|
+
providers: [project.provider],
|
|
4739
4908
|
entries: 0,
|
|
4740
4909
|
byState: emptyStateCounts(),
|
|
4741
4910
|
spendByUnit: { generations: 0, usd: 0, free: 0 },
|
|
@@ -6332,6 +6501,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
6332
6501
|
styles: [ctx.styleId],
|
|
6333
6502
|
...orphan.width === orphan.height ? { size: orphan.width } : { width: orphan.width, height: orphan.height }
|
|
6334
6503
|
};
|
|
6504
|
+
const durableSource = shouldPersistSourceUrl(orphan.previewUrl) ? orphan.previewUrl : null;
|
|
6335
6505
|
upsert(ctx.lock, lockKey(ctx.styleId, assetId), {
|
|
6336
6506
|
styleId: ctx.styleId,
|
|
6337
6507
|
assetId,
|
|
@@ -6346,7 +6516,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
6346
6516
|
candidateIndex: null,
|
|
6347
6517
|
status: "downloaded",
|
|
6348
6518
|
error: null,
|
|
6349
|
-
|
|
6519
|
+
// The bytes are already local. Do not turn a temporary signed
|
|
6520
|
+
// review URL into a credential-bearing committed lock entry.
|
|
6521
|
+
sourceUrl: durableSource,
|
|
6522
|
+
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
6350
6523
|
outputs: [{
|
|
6351
6524
|
path: portableOutputPath(outFile, path19.dirname(ctx.manifestPath)),
|
|
6352
6525
|
sha256: sha256(buf)
|
|
@@ -6463,6 +6636,13 @@ var RecipeSchema = z5.object({
|
|
|
6463
6636
|
digest: z5.string().regex(SHA256_RE)
|
|
6464
6637
|
}).strict()
|
|
6465
6638
|
}).strict().superRefine((recipe, context) => {
|
|
6639
|
+
if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
|
|
6640
|
+
context.addIssue({
|
|
6641
|
+
code: z5.ZodIssueCode.custom,
|
|
6642
|
+
path: ["style", "provider"],
|
|
6643
|
+
message: "must match the recipe provider"
|
|
6644
|
+
});
|
|
6645
|
+
}
|
|
6466
6646
|
const filePaths = recipe.files.map((file) => file.path);
|
|
6467
6647
|
if (new Set(filePaths).size !== filePaths.length) {
|
|
6468
6648
|
context.addIssue({ code: z5.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
|
|
@@ -6718,13 +6898,16 @@ async function installRecipe(target, options = {}) {
|
|
|
6718
6898
|
}
|
|
6719
6899
|
}
|
|
6720
6900
|
const written = await writeArtifactBundle(files);
|
|
6901
|
+
const renderedStyle = renderStyle(loaded.recipe.style, destination, cwd);
|
|
6721
6902
|
return {
|
|
6722
6903
|
recipe: loaded.recipe,
|
|
6723
6904
|
destination,
|
|
6724
6905
|
changed: written.changed,
|
|
6725
6906
|
unchanged: written.unchanged,
|
|
6726
6907
|
styleId: loaded.recipe.styleId,
|
|
6727
|
-
|
|
6908
|
+
// A recipe snippet should remain correct when pasted into a manifest whose
|
|
6909
|
+
// top-level default is another provider.
|
|
6910
|
+
style: { ...renderedStyle, provider: renderedStyle.provider ?? loaded.recipe.provider }
|
|
6728
6911
|
};
|
|
6729
6912
|
}
|
|
6730
6913
|
|
|
@@ -7320,12 +7503,34 @@ function parseArgs(argv) {
|
|
|
7320
7503
|
}
|
|
7321
7504
|
}
|
|
7322
7505
|
let budget;
|
|
7323
|
-
const
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7506
|
+
const providerBudgets = /* @__PURE__ */ Object.create(null);
|
|
7507
|
+
const rawBudgets = rest.flatMap((token, index) => token === "--budget" ? [rest[index + 1]] : []);
|
|
7508
|
+
for (const rawBudget of rawBudgets) {
|
|
7509
|
+
const separator = rawBudget.indexOf("=");
|
|
7510
|
+
if (separator >= 0) {
|
|
7511
|
+
const providerId = rawBudget.slice(0, separator).trim();
|
|
7512
|
+
const rawAmount = rawBudget.slice(separator + 1).trim();
|
|
7513
|
+
const amount2 = Number(rawAmount);
|
|
7514
|
+
if (!providerId || /[=\s]/.test(providerId) || !Number.isFinite(amount2) || amount2 < 0) {
|
|
7515
|
+
throw new Error(
|
|
7516
|
+
`--budget must be a non-negative number or provider=number, got "${rawBudget}".`
|
|
7517
|
+
);
|
|
7518
|
+
}
|
|
7519
|
+
if (Object.hasOwn(providerBudgets, providerId)) {
|
|
7520
|
+
throw new Error(`--budget repeats provider "${providerId}".`);
|
|
7521
|
+
}
|
|
7522
|
+
providerBudgets[providerId] = amount2;
|
|
7523
|
+
continue;
|
|
7524
|
+
}
|
|
7525
|
+
const amount = Number(rawBudget);
|
|
7526
|
+
if (!Number.isFinite(amount) || amount < 0) {
|
|
7327
7527
|
throw new Error(`--budget must be a non-negative number, got "${rawBudget}".`);
|
|
7328
7528
|
}
|
|
7529
|
+
if (budget !== void 0) throw new Error("Only one unkeyed --budget may be passed.");
|
|
7530
|
+
budget = amount;
|
|
7531
|
+
}
|
|
7532
|
+
if (budget !== void 0 && Object.keys(providerBudgets).length) {
|
|
7533
|
+
throw new Error("Do not mix an unkeyed --budget with provider-keyed budgets.");
|
|
7329
7534
|
}
|
|
7330
7535
|
const manifest = get("--manifest") ?? "pixelkiln.manifest.json";
|
|
7331
7536
|
const rawFormat = get("--format");
|
|
@@ -7358,6 +7563,7 @@ function parseArgs(argv) {
|
|
|
7358
7563
|
force: rest.includes("--force"),
|
|
7359
7564
|
yes: rest.includes("--yes") || rest.includes("-y"),
|
|
7360
7565
|
budget,
|
|
7566
|
+
providerBudgets,
|
|
7361
7567
|
dryRun: rest.includes("--dry-run"),
|
|
7362
7568
|
all: rest.includes("--all"),
|
|
7363
7569
|
json: rest.includes("--json"),
|
|
@@ -7456,7 +7662,9 @@ Options
|
|
|
7456
7662
|
--lock <path> Default: pixelkiln.lock.json beside the manifest
|
|
7457
7663
|
--style a,b Restrict to these styles
|
|
7458
7664
|
--only id1,id2 Restrict to these asset ids
|
|
7459
|
-
--budget <n>
|
|
7665
|
+
--budget <n|provider=n> Refuse to exceed one provider ceiling; repeat keyed budgets
|
|
7666
|
+
for a mixed-provider run
|
|
7667
|
+
--provider <id> Choose the account for balance/adopt/salvage/purge in a mixed manifest
|
|
7460
7668
|
--force Regenerate; also replace changed recipe files or quality baselines
|
|
7461
7669
|
--dry-run Never spend; doctor also skips provider connectivity
|
|
7462
7670
|
--all salvage --dry-run: list every unclaimed object, not just the first 30
|
|
@@ -7510,13 +7718,73 @@ function printPlan(plan) {
|
|
|
7510
7718
|
totals: ${counts.ok} ok \xB7 ${counts.missing} missing \xB7 ${counts.untracked} untracked \xB7 ${counts.stale} stale \xB7 ${counts["in-flight"]} in-flight \xB7 ${counts.orphaned} orphaned \xB7 ${counts.recoverable} recoverable \xB7 ${counts.failed} failed`
|
|
7511
7719
|
);
|
|
7512
7720
|
if (plan.actionable.length) {
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7721
|
+
for (const group of plan.groups) {
|
|
7722
|
+
log(
|
|
7723
|
+
` ${group.provider}: ${group.actionable.length} asset(s) \u2014 ${formatCost(group.costUnit, group.cost)}, ${group.candidates} candidate/output image(s)`
|
|
7724
|
+
);
|
|
7725
|
+
}
|
|
7516
7726
|
} else {
|
|
7517
7727
|
log(` nothing to generate`);
|
|
7518
7728
|
}
|
|
7519
7729
|
}
|
|
7730
|
+
function manifestProviderIds(manifest) {
|
|
7731
|
+
return [...new Set(
|
|
7732
|
+
Object.values(manifest.styles).map((style) => style.provider ?? manifest.provider)
|
|
7733
|
+
)].sort();
|
|
7734
|
+
}
|
|
7735
|
+
function specsByRecordedProvider(specs, lock) {
|
|
7736
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
7737
|
+
for (const spec of specs) {
|
|
7738
|
+
const key = lockKey(spec.styleId, spec.assetId);
|
|
7739
|
+
const provider = lock.entries[key]?.provider ?? spec.provider;
|
|
7740
|
+
grouped.set(provider, [...grouped.get(provider) ?? [], spec]);
|
|
7741
|
+
}
|
|
7742
|
+
return grouped;
|
|
7743
|
+
}
|
|
7744
|
+
function accountProviderId(manifest, requested, command) {
|
|
7745
|
+
const providers = manifestProviderIds(manifest);
|
|
7746
|
+
if (requested) {
|
|
7747
|
+
if (!providers.includes(requested)) {
|
|
7748
|
+
throw new Error(
|
|
7749
|
+
`Provider "${requested}" is not used by this manifest. Used: ${providers.join(", ")}.`
|
|
7750
|
+
);
|
|
7751
|
+
}
|
|
7752
|
+
return requested;
|
|
7753
|
+
}
|
|
7754
|
+
if (providers.length > 1) {
|
|
7755
|
+
throw new Error(
|
|
7756
|
+
`${command} is account-scoped and this manifest uses ${providers.join(", ")}. Pass --provider <id>.`
|
|
7757
|
+
);
|
|
7758
|
+
}
|
|
7759
|
+
return providers[0] ?? manifest.provider;
|
|
7760
|
+
}
|
|
7761
|
+
function budgetsForPlan(plan, args) {
|
|
7762
|
+
const providerIds = new Set(plan.groups.map((group) => group.provider));
|
|
7763
|
+
const keyed = Object.entries(args.providerBudgets);
|
|
7764
|
+
for (const [provider] of keyed) {
|
|
7765
|
+
if (!providerIds.has(provider)) {
|
|
7766
|
+
throw new Error(
|
|
7767
|
+
`Budget names provider "${provider}", but this run has work only for ${[...providerIds].join(", ") || "no providers"}.`
|
|
7768
|
+
);
|
|
7769
|
+
}
|
|
7770
|
+
}
|
|
7771
|
+
if (plan.groups.length > 1 && args.budget !== void 0) {
|
|
7772
|
+
throw new Error(
|
|
7773
|
+
"A mixed-provider run needs provider-keyed budgets, for example --budget pixellab=40 --budget retrodiffusion=1.25."
|
|
7774
|
+
);
|
|
7775
|
+
}
|
|
7776
|
+
const out = /* @__PURE__ */ new Map();
|
|
7777
|
+
for (const group of plan.groups) {
|
|
7778
|
+
const ceiling = args.providerBudgets[group.provider];
|
|
7779
|
+
if (plan.groups.length > 1 && group.costUnit !== "free" && group.cost > 0 && ceiling === void 0) {
|
|
7780
|
+
throw new Error(
|
|
7781
|
+
`Mixed-provider run is missing --budget ${group.provider}=<amount> for ${formatCost(group.costUnit, group.cost)} of planned work.`
|
|
7782
|
+
);
|
|
7783
|
+
}
|
|
7784
|
+
out.set(group.provider, ceiling ?? (plan.groups.length === 1 ? args.budget : void 0));
|
|
7785
|
+
}
|
|
7786
|
+
return out;
|
|
7787
|
+
}
|
|
7520
7788
|
async function confirm(question, auto) {
|
|
7521
7789
|
if (auto) return true;
|
|
7522
7790
|
if (!process.stdin.isTTY) {
|
|
@@ -7766,7 +8034,8 @@ async function main() {
|
|
|
7766
8034
|
loadEnvFiles(path22.dirname(path22.resolve(args.manifest)));
|
|
7767
8035
|
loadEnvFiles(process.cwd());
|
|
7768
8036
|
const loaded2 = await loadManifest(args.manifest);
|
|
7769
|
-
const
|
|
8037
|
+
const selectedProvider = accountProviderId(loaded2.manifest, args.provider, "balance");
|
|
8038
|
+
const p = createProvider(selectedProvider, "online");
|
|
7770
8039
|
const b = await requireBalance(p)();
|
|
7771
8040
|
log(` provider: ${p.id}`);
|
|
7772
8041
|
log(` plan: ${b.plan ?? "n/a"}`);
|
|
@@ -7955,8 +8224,10 @@ async function main() {
|
|
|
7955
8224
|
${p.id} \u2014 ERROR: ${p.error}`);
|
|
7956
8225
|
continue;
|
|
7957
8226
|
}
|
|
7958
|
-
log(
|
|
7959
|
-
|
|
8227
|
+
log(
|
|
8228
|
+
`
|
|
8229
|
+
${p.id} (${p.providers.join(" + ")}${p.account ? `, ${p.account}` : ""})`
|
|
8230
|
+
);
|
|
7960
8231
|
log(` ${p.entries} lock entries`);
|
|
7961
8232
|
for (const [state, n] of Object.entries(p.byState)) if (n) log(` ${state.padEnd(12)} ${n}`);
|
|
7962
8233
|
for (const [unit, amount] of Object.entries(p.spendByUnit).sort()) {
|
|
@@ -8001,12 +8272,34 @@ async function main() {
|
|
|
8001
8272
|
const envFiles = [...loadEnvFiles(manifestDir)];
|
|
8002
8273
|
if (path22.resolve(process.cwd()) !== manifestDir) envFiles.push(...loadEnvFiles(process.cwd()));
|
|
8003
8274
|
const loaded = await loadManifest(args.manifest);
|
|
8004
|
-
|
|
8005
|
-
const specs = await resolveSpecs(loaded, {
|
|
8275
|
+
let specs = await resolveSpecs(loaded, {
|
|
8006
8276
|
styles: args.styles,
|
|
8007
|
-
assets: args.assets
|
|
8008
|
-
provider: estimator
|
|
8277
|
+
assets: args.assets
|
|
8009
8278
|
});
|
|
8279
|
+
const accountCommands = /* @__PURE__ */ new Set(["adopt", "salvage", "purge"]);
|
|
8280
|
+
const selectedAccountProvider = accountCommands.has(args.command) ? accountProviderId(loaded.manifest, args.provider, args.command) : void 0;
|
|
8281
|
+
if (args.provider && !selectedAccountProvider) {
|
|
8282
|
+
throw new Error(
|
|
8283
|
+
"--provider is only used by balance, adopt, salvage, purge, or workspace add."
|
|
8284
|
+
);
|
|
8285
|
+
}
|
|
8286
|
+
if (selectedAccountProvider) {
|
|
8287
|
+
specs = specs.filter((spec) => spec.provider === selectedAccountProvider);
|
|
8288
|
+
}
|
|
8289
|
+
const accountManifest = selectedAccountProvider ? {
|
|
8290
|
+
...loaded.manifest,
|
|
8291
|
+
provider: selectedAccountProvider,
|
|
8292
|
+
styles: Object.fromEntries(
|
|
8293
|
+
Object.entries(loaded.manifest.styles).filter(
|
|
8294
|
+
([, style]) => (style.provider ?? loaded.manifest.provider) === selectedAccountProvider
|
|
8295
|
+
)
|
|
8296
|
+
)
|
|
8297
|
+
} : loaded.manifest;
|
|
8298
|
+
if (selectedAccountProvider && args.styles.some((styleId) => !accountManifest.styles[styleId])) {
|
|
8299
|
+
throw new Error(
|
|
8300
|
+
`Selected style is not assigned to provider "${selectedAccountProvider}".`
|
|
8301
|
+
);
|
|
8302
|
+
}
|
|
8010
8303
|
const lock = await loadLock(args.lock);
|
|
8011
8304
|
normalizeLockOutputPaths(lock, specs);
|
|
8012
8305
|
if (args.command === "status") {
|
|
@@ -8031,14 +8324,23 @@ async function main() {
|
|
|
8031
8324
|
}
|
|
8032
8325
|
const plan = await buildPlan(specs, lock, { force: args.force });
|
|
8033
8326
|
if (args.command === "doctor") {
|
|
8034
|
-
const
|
|
8035
|
-
const
|
|
8036
|
-
|
|
8327
|
+
const selectedStyleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
8328
|
+
const providerIds = [...new Set(
|
|
8329
|
+
selectedStyleIds.map((styleId) => loaded.manifest.styles[styleId]?.provider ?? loaded.manifest.provider)
|
|
8330
|
+
)].sort();
|
|
8331
|
+
const providers2 = providerIds.map((id) => {
|
|
8332
|
+
const factory = providerFactory(id);
|
|
8333
|
+
const apiKeyPresent = !factory.credentialEnv || Boolean(process.env[factory.credentialEnv]);
|
|
8334
|
+
return {
|
|
8335
|
+
id,
|
|
8336
|
+
apiKeyPresent,
|
|
8337
|
+
credentialEnv: factory.credentialEnv,
|
|
8338
|
+
provider: !args.dryRun && apiKeyPresent ? createProvider(id, "online") : void 0
|
|
8339
|
+
};
|
|
8340
|
+
});
|
|
8037
8341
|
const report = await doctor(loaded, specs, lock, args.lock, {
|
|
8038
|
-
|
|
8039
|
-
offline: args.dryRun
|
|
8040
|
-
apiKeyPresent,
|
|
8041
|
-
credentialEnv: factory.credentialEnv
|
|
8342
|
+
providers: providers2,
|
|
8343
|
+
offline: args.dryRun
|
|
8042
8344
|
});
|
|
8043
8345
|
if (args.json) {
|
|
8044
8346
|
log(JSON.stringify(report, null, 2));
|
|
@@ -8058,6 +8360,13 @@ async function main() {
|
|
|
8058
8360
|
cost: plan.cost,
|
|
8059
8361
|
costUnit: plan.costUnit,
|
|
8060
8362
|
candidates: plan.candidates,
|
|
8363
|
+
groups: plan.groups.map((group) => ({
|
|
8364
|
+
provider: group.provider,
|
|
8365
|
+
cost: group.cost,
|
|
8366
|
+
costUnit: group.costUnit,
|
|
8367
|
+
candidates: group.candidates,
|
|
8368
|
+
actionable: group.actionable.map((item) => item.key)
|
|
8369
|
+
})),
|
|
8061
8370
|
actionable: plan.actionable.map((i) => i.key),
|
|
8062
8371
|
items: plan.items.map(({ key, state, reason }) => ({ key, state, reason }))
|
|
8063
8372
|
}, null, 2));
|
|
@@ -8072,7 +8381,7 @@ async function main() {
|
|
|
8072
8381
|
for (const item of plan.items) {
|
|
8073
8382
|
if (item.state !== "stale") continue;
|
|
8074
8383
|
const entry = lock.entries[item.key];
|
|
8075
|
-
if (!entry || entry.outputs.length === 0) continue;
|
|
8384
|
+
if (!entry || entry.provider !== item.spec.provider || entry.outputs.length === 0) continue;
|
|
8076
8385
|
let intact = true;
|
|
8077
8386
|
for (const output of entry.outputs) {
|
|
8078
8387
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
@@ -8338,8 +8647,18 @@ async function main() {
|
|
|
8338
8647
|
}
|
|
8339
8648
|
return;
|
|
8340
8649
|
}
|
|
8341
|
-
const
|
|
8650
|
+
const providerMode = args.command === "restore" || args.command === "fetch" && !args.tag ? "downloads" : "online";
|
|
8651
|
+
const providers = /* @__PURE__ */ new Map();
|
|
8652
|
+
const providerFor = (id) => {
|
|
8653
|
+
let resolved = providers.get(id);
|
|
8654
|
+
if (!resolved) {
|
|
8655
|
+
resolved = createProvider(id, providerMode);
|
|
8656
|
+
providers.set(id, resolved);
|
|
8657
|
+
}
|
|
8658
|
+
return resolved;
|
|
8659
|
+
};
|
|
8342
8660
|
if (args.command === "adopt") {
|
|
8661
|
+
const provider = providerFor(selectedAccountProvider);
|
|
8343
8662
|
log(`
|
|
8344
8663
|
Reconciling account objects against files already on disk\u2026`);
|
|
8345
8664
|
const res = await adopt(provider, specs, lock, args.lock, { onProgress: log });
|
|
@@ -8374,15 +8693,16 @@ async function main() {
|
|
|
8374
8693
|
}
|
|
8375
8694
|
if (args.writePrompts) {
|
|
8376
8695
|
const { filled, stillEmpty } = await writePromptsBack(path22.resolve(args.manifest), lock, {
|
|
8377
|
-
onProgress: log
|
|
8696
|
+
onProgress: log,
|
|
8697
|
+
provider: provider.id,
|
|
8698
|
+
assetIds: specs.map((spec) => spec.assetId)
|
|
8378
8699
|
});
|
|
8379
8700
|
log(` recovered ${filled} prompt(s) into ${path22.relative(process.cwd(), args.manifest)}`);
|
|
8380
8701
|
const reloaded = await loadManifest(args.manifest);
|
|
8381
|
-
const rebased = await resolveSpecs(reloaded, {
|
|
8702
|
+
const rebased = (await resolveSpecs(reloaded, {
|
|
8382
8703
|
styles: args.styles,
|
|
8383
|
-
assets: args.assets
|
|
8384
|
-
|
|
8385
|
-
});
|
|
8704
|
+
assets: args.assets
|
|
8705
|
+
})).filter((spec) => spec.provider === provider.id);
|
|
8386
8706
|
let n = 0;
|
|
8387
8707
|
for (const spec of rebased) {
|
|
8388
8708
|
const key = lockKey(spec.styleId, spec.assetId);
|
|
@@ -8403,6 +8723,7 @@ async function main() {
|
|
|
8403
8723
|
return;
|
|
8404
8724
|
}
|
|
8405
8725
|
if (args.command === "salvage") {
|
|
8726
|
+
const provider = providerFor(selectedAccountProvider);
|
|
8406
8727
|
const jsonMode = args.dryRun && args.json;
|
|
8407
8728
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
8408
8729
|
const ownLock = path22.resolve(args.lock);
|
|
@@ -8440,7 +8761,7 @@ async function main() {
|
|
|
8440
8761
|
\`pixelkiln workspace add ${args.manifest}\` would keep it aggregated too.`
|
|
8441
8762
|
);
|
|
8442
8763
|
}
|
|
8443
|
-
const claimed = await loadClaims(lockPaths);
|
|
8764
|
+
const claimed = await loadClaims(lockPaths, { provider: provider.id });
|
|
8444
8765
|
const { orphans, total } = await findOrphans(provider, claimed, { onProgress: diag });
|
|
8445
8766
|
diag(` ${claimed.size} claimed \xB7 ${orphans.length} unclaimed of ${total}`);
|
|
8446
8767
|
if (!orphans.length) {
|
|
@@ -8454,8 +8775,8 @@ async function main() {
|
|
|
8454
8775
|
workspaceProjects.map((p) => resolveProject(workspaceDir, p).manifestPath),
|
|
8455
8776
|
args.claims
|
|
8456
8777
|
);
|
|
8457
|
-
const { matched, unmatched, elsewhere } = groupOrphansByStyle(orphans,
|
|
8458
|
-
const multiStyle = Object.keys(
|
|
8778
|
+
const { matched, unmatched, elsewhere } = groupOrphansByStyle(orphans, accountManifest, siblings);
|
|
8779
|
+
const multiStyle = Object.keys(accountManifest.styles).length > 1;
|
|
8459
8780
|
if (multiStyle) {
|
|
8460
8781
|
diag(`
|
|
8461
8782
|
by style (matched against each style's prompt prefix/suffix):`);
|
|
@@ -8495,7 +8816,7 @@ async function main() {
|
|
|
8495
8816
|
}
|
|
8496
8817
|
const rebaseline = async () => {
|
|
8497
8818
|
const reloaded = await loadManifest(args.manifest);
|
|
8498
|
-
const rebased = await resolveSpecs(reloaded
|
|
8819
|
+
const rebased = (await resolveSpecs(reloaded)).filter((spec) => spec.provider === provider.id);
|
|
8499
8820
|
let n = 0;
|
|
8500
8821
|
for (const spec of rebased) {
|
|
8501
8822
|
const key = lockKey(spec.styleId, spec.assetId);
|
|
@@ -8508,13 +8829,13 @@ async function main() {
|
|
|
8508
8829
|
if (n) log(` baselined ${n} imported asset(s) against the manifest`);
|
|
8509
8830
|
};
|
|
8510
8831
|
const runOne = async (styleId, list) => {
|
|
8511
|
-
const style =
|
|
8832
|
+
const style = accountManifest.styles[styleId];
|
|
8512
8833
|
const res = await runSalvage(
|
|
8513
8834
|
provider,
|
|
8514
8835
|
list,
|
|
8515
8836
|
{
|
|
8516
8837
|
manifestPath: loaded.path,
|
|
8517
|
-
manifest:
|
|
8838
|
+
manifest: accountManifest,
|
|
8518
8839
|
styleId,
|
|
8519
8840
|
importDir: path22.resolve(loaded.root, style.outDir),
|
|
8520
8841
|
lock,
|
|
@@ -8561,6 +8882,7 @@ async function main() {
|
|
|
8561
8882
|
return;
|
|
8562
8883
|
}
|
|
8563
8884
|
if (args.command === "purge") {
|
|
8885
|
+
const provider = providerFor(selectedAccountProvider);
|
|
8564
8886
|
const doomed = [];
|
|
8565
8887
|
for await (const obj of requireList(provider)()) {
|
|
8566
8888
|
if (obj.tags.includes("pixelkiln:discard")) {
|
|
@@ -8604,8 +8926,13 @@ async function main() {
|
|
|
8604
8926
|
return;
|
|
8605
8927
|
}
|
|
8606
8928
|
if (args.command === "tag") {
|
|
8607
|
-
|
|
8608
|
-
|
|
8929
|
+
let tagged = 0;
|
|
8930
|
+
for (const [providerId, providerSpecsForRun] of specsByRecordedProvider(specs, lock)) {
|
|
8931
|
+
tagged += await pushTags(providerFor(providerId), providerSpecsForRun, lock, {
|
|
8932
|
+
onProgress: log
|
|
8933
|
+
});
|
|
8934
|
+
}
|
|
8935
|
+
log(` tagged ${tagged} object(s)`);
|
|
8609
8936
|
return;
|
|
8610
8937
|
}
|
|
8611
8938
|
if (args.command === "submit" || args.command === "gen") {
|
|
@@ -8616,26 +8943,43 @@ async function main() {
|
|
|
8616
8943
|
return;
|
|
8617
8944
|
}
|
|
8618
8945
|
if (plan.actionable.length) {
|
|
8619
|
-
const
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8946
|
+
const budgets = budgetsForPlan(plan, args);
|
|
8947
|
+
const balances = /* @__PURE__ */ new Map();
|
|
8948
|
+
for (const group of plan.groups) {
|
|
8949
|
+
const groupProvider = providerFor(group.provider);
|
|
8950
|
+
const balance = groupProvider.balance ? await groupProvider.balance() : null;
|
|
8951
|
+
balances.set(group.provider, balance);
|
|
8952
|
+
if (balance) {
|
|
8953
|
+
log(
|
|
8954
|
+
`
|
|
8955
|
+
balance: ${formatCost(balance.unit, balance.remaining)} remaining (${group.provider})`
|
|
8956
|
+
);
|
|
8957
|
+
if (balance.unit !== group.costUnit) {
|
|
8958
|
+
throw new Error(
|
|
8959
|
+
`Provider ${group.provider} estimate unit ${group.costUnit} does not match balance unit ${balance.unit}.`
|
|
8960
|
+
);
|
|
8961
|
+
}
|
|
8962
|
+
if (balance.unit !== "free" && group.cost > balance.remaining) {
|
|
8963
|
+
throw new Error(
|
|
8964
|
+
`${group.provider} needs ${formatCost(balance.unit, group.cost)} but only ${formatCost(balance.unit, balance.remaining)} remain.`
|
|
8965
|
+
);
|
|
8966
|
+
}
|
|
8967
|
+
} else {
|
|
8968
|
+
log(
|
|
8969
|
+
`
|
|
8970
|
+
${group.provider} does not expose an account balance; enforcing its run budget`
|
|
8626
8971
|
);
|
|
8627
8972
|
}
|
|
8628
|
-
|
|
8973
|
+
const ceiling = budgets.get(group.provider);
|
|
8974
|
+
if (ceiling !== void 0 && group.cost > ceiling) {
|
|
8629
8975
|
throw new Error(
|
|
8630
|
-
|
|
8976
|
+
`${group.provider} would spend ${formatCost(group.costUnit, group.cost)} but its budget is ${formatCost(group.costUnit, ceiling)}.`
|
|
8631
8977
|
);
|
|
8632
8978
|
}
|
|
8633
|
-
} else {
|
|
8634
|
-
log(`
|
|
8635
|
-
${provider.id} does not expose an account balance; enforcing the explicit run budget`);
|
|
8636
8979
|
}
|
|
8980
|
+
const spendSummary = plan.groups.map((group) => `${group.provider} ${formatCost(group.costUnit, group.cost)}`).join("; ");
|
|
8637
8981
|
const ok = await confirm(
|
|
8638
|
-
` Spend ${
|
|
8982
|
+
` Spend ${spendSummary} on ${plan.actionable.length} asset(s)?`,
|
|
8639
8983
|
args.yes
|
|
8640
8984
|
);
|
|
8641
8985
|
if (!ok) {
|
|
@@ -8644,69 +8988,104 @@ async function main() {
|
|
|
8644
8988
|
}
|
|
8645
8989
|
log(`
|
|
8646
8990
|
submitting\u2026`);
|
|
8647
|
-
const
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8991
|
+
for (const group of plan.groups) {
|
|
8992
|
+
const groupProvider = providerFor(group.provider);
|
|
8993
|
+
const res = await submit(groupProvider, loaded, group.actionable, lock, args.lock, {
|
|
8994
|
+
budget: budgets.get(group.provider),
|
|
8995
|
+
onProgress: log
|
|
8996
|
+
});
|
|
8997
|
+
log(
|
|
8998
|
+
`
|
|
8999
|
+
${group.provider}: submitted ${res.submitted}, failed ${res.failed}, estimated ${formatCost(res.unit, res.spent)}`
|
|
9000
|
+
);
|
|
9001
|
+
try {
|
|
9002
|
+
const before = balances.get(group.provider);
|
|
9003
|
+
const after = before && groupProvider.balance ? await groupProvider.balance() : null;
|
|
9004
|
+
if (!before || !after) throw new Error("balance reporting is unsupported");
|
|
9005
|
+
const measured = measureBalanceChange(before, after);
|
|
9006
|
+
if (measured) {
|
|
9007
|
+
const movement = measured.credited ? `${formatCost(measured.unit, measured.credited)} credited` : `${formatCost(measured.unit, measured.spent)} consumed`;
|
|
9008
|
+
log(` ${group.provider} balance change: ${movement}`);
|
|
9009
|
+
} else {
|
|
9010
|
+
log(` ${group.provider} changed balance units; no delta reported`);
|
|
9011
|
+
}
|
|
9012
|
+
} catch (err) {
|
|
9013
|
+
log(
|
|
9014
|
+
` ${group.provider} balance recheck unavailable: ${err instanceof Error ? err.message : String(err)}`
|
|
9015
|
+
);
|
|
8664
9016
|
}
|
|
8665
|
-
|
|
8666
|
-
log(` provider balance recheck unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
9017
|
+
if (res.failed) process.exitCode = 1;
|
|
8667
9018
|
}
|
|
8668
|
-
if (res.failed) process.exitCode = 1;
|
|
8669
9019
|
}
|
|
8670
9020
|
if (args.command === "submit") return;
|
|
8671
9021
|
}
|
|
8672
9022
|
if (args.command === "poll" || args.command === "gen") {
|
|
8673
9023
|
log(`
|
|
8674
9024
|
polling\u2026`);
|
|
8675
|
-
const
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
9025
|
+
const total = { completed: 0, review: 0, failed: 0, stillRunning: 0 };
|
|
9026
|
+
for (const [providerId, providerSpecsForRun] of specsByRecordedProvider(specs, lock)) {
|
|
9027
|
+
const res = await poll(providerFor(providerId), lock, args.lock, {
|
|
9028
|
+
onProgress: log,
|
|
9029
|
+
specs: providerSpecsForRun
|
|
9030
|
+
});
|
|
9031
|
+
total.completed += res.completed;
|
|
9032
|
+
total.review += res.review;
|
|
9033
|
+
total.failed += res.failed;
|
|
9034
|
+
total.stillRunning += res.stillRunning;
|
|
9035
|
+
}
|
|
9036
|
+
log(
|
|
9037
|
+
`
|
|
9038
|
+
${total.completed} ready \xB7 ${total.review} awaiting selection \xB7 ${total.failed} failed`
|
|
9039
|
+
);
|
|
9040
|
+
if (total.failed || total.stillRunning) process.exitCode = 1;
|
|
8679
9041
|
if (args.command === "poll") return;
|
|
8680
9042
|
}
|
|
8681
9043
|
if (args.command === "pick" || args.command === "gen") {
|
|
8682
|
-
const
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
9044
|
+
const total = { selected: 0, skipped: 0 };
|
|
9045
|
+
for (const [providerId, providerSpecsForRun] of specsByRecordedProvider(specs, lock)) {
|
|
9046
|
+
const res = await runPicker(providerFor(providerId), lock, args.lock, {
|
|
9047
|
+
port: args.port,
|
|
9048
|
+
open: !args.noOpen,
|
|
9049
|
+
onProgress: log,
|
|
9050
|
+
keys: providerSpecsForRun.map((spec) => lockKey(spec.styleId, spec.assetId))
|
|
9051
|
+
});
|
|
9052
|
+
total.selected += res.selected;
|
|
9053
|
+
total.skipped += res.skipped;
|
|
9054
|
+
}
|
|
9055
|
+
if (total.selected === 0 && total.skipped === 0) {
|
|
8688
9056
|
log(` nothing awaiting selection`);
|
|
8689
9057
|
} else {
|
|
8690
9058
|
log(`
|
|
8691
|
-
selected ${
|
|
9059
|
+
selected ${total.selected}, left in review ${total.skipped}`);
|
|
8692
9060
|
}
|
|
8693
|
-
if (args.command === "gen" &&
|
|
9061
|
+
if (args.command === "gen" && total.skipped) process.exitCode = 1;
|
|
8694
9062
|
if (args.command === "pick") return;
|
|
8695
9063
|
}
|
|
8696
9064
|
if (args.command === "fetch" || args.command === "restore" || args.command === "gen") {
|
|
8697
9065
|
log(`
|
|
8698
9066
|
downloading\u2026`);
|
|
8699
|
-
const
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
9067
|
+
const total = { downloaded: 0, skipped: 0, failed: 0, tagged: 0 };
|
|
9068
|
+
for (const [providerId, providerSpecsForRun] of specsByRecordedProvider(specs, lock)) {
|
|
9069
|
+
const groupProvider = providerFor(providerId);
|
|
9070
|
+
const res = await fetchAssets(groupProvider, providerSpecsForRun, lock, args.lock, {
|
|
9071
|
+
onProgress: log,
|
|
9072
|
+
repair: args.command === "restore"
|
|
9073
|
+
});
|
|
9074
|
+
total.downloaded += res.downloaded;
|
|
9075
|
+
total.skipped += res.skipped;
|
|
9076
|
+
total.failed += res.failed;
|
|
9077
|
+
if (args.tag) {
|
|
9078
|
+
total.tagged += await pushTags(groupProvider, providerSpecsForRun, lock, {
|
|
9079
|
+
onProgress: log
|
|
9080
|
+
});
|
|
9081
|
+
}
|
|
8709
9082
|
}
|
|
9083
|
+
log(
|
|
9084
|
+
`
|
|
9085
|
+
downloaded ${total.downloaded}, skipped ${total.skipped}, failed ${total.failed}`
|
|
9086
|
+
);
|
|
9087
|
+
if (total.failed) process.exitCode = 1;
|
|
9088
|
+
if (args.tag) log(` tagged ${total.tagged} object(s) upstream`);
|
|
8710
9089
|
await saveLock(args.lock, lock);
|
|
8711
9090
|
log(` lockfile written: ${args.lock}`);
|
|
8712
9091
|
return;
|