pixelkiln 0.12.1 → 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/PROVIDERS.md +43 -69
- package/README.md +8 -7
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +436 -136
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +144 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -10
- package/dist/index.d.ts +64 -10
- package/dist/index.js +144 -44
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +8 -7
- package/docs/ARCHITECTURE.md +5 -2
- package/docs/CLI.md +32 -18
- package/docs/COMFYUI.md +3 -1
- package/docs/GETTING_STARTED.md +9 -3
- package/docs/LIBRARY.md +22 -11
- package/docs/MANIFEST.md +11 -7
- package/docs/MIXED_PROVIDERS.md +134 -0
- package/docs/PIXELLAB.md +3 -0
- package/docs/README.md +1 -0
- package/docs/RECIPES.md +1 -1
- package/docs/RECOVERY.md +12 -4
- package/docs/RETRO_DIFFUSION.md +3 -2
- package/package.json +1 -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/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"),
|
|
@@ -2392,7 +2394,17 @@ ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
|
2392
2394
|
}
|
|
2393
2395
|
async function resolveSpecs(loaded, filter) {
|
|
2394
2396
|
const { manifest, root } = loaded;
|
|
2395
|
-
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
|
+
};
|
|
2396
2408
|
const specs = [];
|
|
2397
2409
|
const styleIds = Object.keys(manifest.styles).filter(
|
|
2398
2410
|
(id) => !filter?.styles?.length || filter.styles.includes(id)
|
|
@@ -2428,6 +2440,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
2428
2440
|
}
|
|
2429
2441
|
for (const styleId of styleIds) {
|
|
2430
2442
|
const style = manifest.styles[styleId];
|
|
2443
|
+
const activeProvider = providerFor(style.provider ?? manifest.provider);
|
|
2431
2444
|
const rawProviderOptions = style.providerOptions[activeProvider.id] ?? {};
|
|
2432
2445
|
const optionResolution = activeProvider.resolveOptions ? await activeProvider.resolveOptions(rawProviderOptions, { root, styleId }) : { options: rawProviderOptions };
|
|
2433
2446
|
const providerOptions = optionResolution.options;
|
|
@@ -3171,13 +3184,39 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
3171
3184
|
items.push({ spec, key, state, reason });
|
|
3172
3185
|
}
|
|
3173
3186
|
const actionable = items.filter((i) => i.state === "missing" || i.state === "stale" || i.state === "failed");
|
|
3174
|
-
const
|
|
3175
|
-
|
|
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;
|
|
3176
3214
|
return {
|
|
3177
3215
|
items,
|
|
3178
3216
|
actionable,
|
|
3179
|
-
|
|
3180
|
-
|
|
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")),
|
|
3181
3220
|
candidates: actionable.reduce((sum, i) => sum + i.spec.candidates, 0)
|
|
3182
3221
|
};
|
|
3183
3222
|
}
|
|
@@ -3284,6 +3323,10 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
3284
3323
|
submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3285
3324
|
cost: estimate.amount,
|
|
3286
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,
|
|
3287
3330
|
downloadedAt: null
|
|
3288
3331
|
});
|
|
3289
3332
|
await saveLock(lockPath, lock);
|
|
@@ -3296,8 +3339,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
3296
3339
|
// A multi-candidate generator routes through review; record the parent
|
|
3297
3340
|
// so `pick` knows where to look.
|
|
3298
3341
|
reviewObjectId: estimate.candidates > 1 && !spec.tileFeature ? jobId : null,
|
|
3299
|
-
status: "processing"
|
|
3300
|
-
provider: provider.id
|
|
3342
|
+
status: "processing"
|
|
3301
3343
|
});
|
|
3302
3344
|
inFlight.set(jobId, spec);
|
|
3303
3345
|
submitted++;
|
|
@@ -3325,9 +3367,10 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
3325
3367
|
});
|
|
3326
3368
|
const started = Date.now();
|
|
3327
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;
|
|
3328
3371
|
const result = { review: 0, completed: 0, failed: 0, stillRunning: 0 };
|
|
3329
3372
|
const pending = () => Object.entries(lock.entries).filter(
|
|
3330
|
-
([, 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")
|
|
3331
3374
|
);
|
|
3332
3375
|
while (pending().length > 0) {
|
|
3333
3376
|
if (Date.now() - started > timeout) {
|
|
@@ -3429,6 +3472,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3429
3472
|
normalizeLockOutputPaths(lock, specs);
|
|
3430
3473
|
const cacheDir = opts.cacheDir === false ? null : path9.resolve(opts.cacheDir ?? path9.join(path9.dirname(lockPath), ".pixelkiln", "cache"));
|
|
3431
3474
|
const pending = Object.entries(lock.entries).filter(([key, e]) => {
|
|
3475
|
+
if (e.provider !== provider.id || !specByKey.has(key)) return false;
|
|
3432
3476
|
if (e.status === "selected" || e.status === "download-failed") return true;
|
|
3433
3477
|
if (!opts.repair || e.status !== "downloaded") return false;
|
|
3434
3478
|
const spec = specByKey.get(key);
|
|
@@ -3595,7 +3639,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
|
|
|
3595
3639
|
let tagged = 0;
|
|
3596
3640
|
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
3597
3641
|
const spec = specByKey.get(key);
|
|
3598
|
-
if (!spec || !entry.objectId) continue;
|
|
3642
|
+
if (!spec || entry.provider !== provider.id || !entry.objectId) continue;
|
|
3599
3643
|
if (entry.generator === "pixflux") continue;
|
|
3600
3644
|
if (!provider.setTags) return tagged;
|
|
3601
3645
|
try {
|
|
@@ -3701,30 +3745,51 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
3701
3745
|
unsettled ? "warning" : "ok",
|
|
3702
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"
|
|
3703
3747
|
);
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
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
|
+
}
|
|
3728
3793
|
}
|
|
3729
3794
|
}
|
|
3730
3795
|
return { ok: !checks.some((c) => c.level === "error"), checks };
|
|
@@ -3876,6 +3941,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3876
3941
|
ambiguous.push(`${key}: already mapped to ${existing.objectId}, also matches ${obj.id}`);
|
|
3877
3942
|
continue;
|
|
3878
3943
|
}
|
|
3944
|
+
const durableSource = shouldPersistSourceUrl(url) ? url : null;
|
|
3879
3945
|
upsert(lock, key, {
|
|
3880
3946
|
styleId: spec.styleId,
|
|
3881
3947
|
assetId: spec.assetId,
|
|
@@ -3892,7 +3958,8 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3892
3958
|
candidateIndex: null,
|
|
3893
3959
|
status: "downloaded",
|
|
3894
3960
|
error: null,
|
|
3895
|
-
sourceUrl:
|
|
3961
|
+
sourceUrl: durableSource,
|
|
3962
|
+
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
3896
3963
|
outputs: [{ path: portableOutputPath(spec.outFile, spec.root), sha256: hash }],
|
|
3897
3964
|
submittedAt: obj.createdAt,
|
|
3898
3965
|
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3942,9 +4009,12 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
|
|
|
3942
4009
|
async function writePromptsBack(manifestPath, lock, opts = {}) {
|
|
3943
4010
|
const log2 = opts.onProgress ?? (() => {
|
|
3944
4011
|
});
|
|
4012
|
+
const selectedAssets = opts.assetIds ? new Set(opts.assetIds) : null;
|
|
3945
4013
|
const raw = JSON.parse(await readFile6(manifestPath, "utf8"));
|
|
3946
4014
|
const best = /* @__PURE__ */ new Map();
|
|
3947
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;
|
|
3948
4018
|
if (!entry.objectId || !entry.prompt) continue;
|
|
3949
4019
|
const current = best.get(entry.assetId);
|
|
3950
4020
|
if (!current || entry.prompt.length > current.length) best.set(entry.assetId, entry.prompt);
|
|
@@ -3952,6 +4022,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
|
|
|
3952
4022
|
let filled = 0;
|
|
3953
4023
|
const stillEmpty = [];
|
|
3954
4024
|
for (const [id, asset] of Object.entries(raw.assets)) {
|
|
4025
|
+
if (selectedAssets && !selectedAssets.has(id)) continue;
|
|
3955
4026
|
if (asset.prompt && asset.prompt.trim()) continue;
|
|
3956
4027
|
const recovered = best.get(id);
|
|
3957
4028
|
if (recovered) {
|
|
@@ -4267,8 +4338,10 @@ function serveReviewPage(opts) {
|
|
|
4267
4338
|
async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
4268
4339
|
const log2 = opts.onProgress ?? (() => {
|
|
4269
4340
|
});
|
|
4341
|
+
const selectedKeys = opts.keys ? new Set(opts.keys) : null;
|
|
4270
4342
|
const groups = [];
|
|
4271
4343
|
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
4344
|
+
if (entry.provider !== provider.id || selectedKeys && !selectedKeys.has(key)) continue;
|
|
4272
4345
|
if (entry.status !== "review" || !entry.reviewObjectId) continue;
|
|
4273
4346
|
try {
|
|
4274
4347
|
const state = await provider.poll(entry.reviewObjectId, entry.generator);
|
|
@@ -4434,7 +4507,10 @@ async function writeManifestFile(target, manifest) {
|
|
|
4434
4507
|
import { readFile as readFile8 } from "fs/promises";
|
|
4435
4508
|
import { existsSync as existsSync11 } from "fs";
|
|
4436
4509
|
import path13 from "path";
|
|
4437
|
-
|
|
4510
|
+
function providerClaimId(provider, objectId) {
|
|
4511
|
+
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
4512
|
+
}
|
|
4513
|
+
async function loadClaims(lockPaths, opts = {}) {
|
|
4438
4514
|
const claimed = /* @__PURE__ */ new Set();
|
|
4439
4515
|
for (const p of lockPaths) {
|
|
4440
4516
|
if (!existsSync11(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
@@ -4445,9 +4521,13 @@ async function loadClaims(lockPaths) {
|
|
|
4445
4521
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
4446
4522
|
}
|
|
4447
4523
|
for (const entry of Object.values(parsed.entries)) {
|
|
4448
|
-
if (entry.
|
|
4449
|
-
|
|
4450
|
-
|
|
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);
|
|
4451
4531
|
}
|
|
4452
4532
|
}
|
|
4453
4533
|
return claimed;
|
|
@@ -4458,7 +4538,9 @@ async function findOrphans(provider, claimed, opts = {}) {
|
|
|
4458
4538
|
const all = [];
|
|
4459
4539
|
for await (const obj of requireList(provider)()) all.push(obj);
|
|
4460
4540
|
log2(` scanned ${all.length} object(s) on the account`);
|
|
4461
|
-
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) => ({
|
|
4462
4544
|
id: o.id,
|
|
4463
4545
|
prompt: o.prompt || "(no prompt recorded)",
|
|
4464
4546
|
width: o.width,
|
|
@@ -4753,7 +4835,7 @@ async function workspaceClaims(ws, dir) {
|
|
|
4753
4835
|
lockPaths.push(lockPath);
|
|
4754
4836
|
let projectClaims;
|
|
4755
4837
|
try {
|
|
4756
|
-
projectClaims = await loadClaims([lockPath]);
|
|
4838
|
+
projectClaims = await loadClaims([lockPath], { qualify: true });
|
|
4757
4839
|
} catch (err) {
|
|
4758
4840
|
throw new Error(
|
|
4759
4841
|
`Project "${project.id}" lockfile is unreadable: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -4791,9 +4873,14 @@ async function workspaceStatus(ws, dir) {
|
|
|
4791
4873
|
lock: lockPath
|
|
4792
4874
|
};
|
|
4793
4875
|
try {
|
|
4794
|
-
const provider = createProvider(project.provider, "offline");
|
|
4795
4876
|
const loaded = await loadManifest(manifestPath);
|
|
4796
|
-
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);
|
|
4797
4884
|
const lock = await loadLock(lockPath);
|
|
4798
4885
|
normalizeLockOutputPaths(lock, specs);
|
|
4799
4886
|
const plan = await buildPlan(specs, lock);
|
|
@@ -4807,6 +4894,8 @@ async function workspaceStatus(ws, dir) {
|
|
|
4807
4894
|
}
|
|
4808
4895
|
projects.push({
|
|
4809
4896
|
...base,
|
|
4897
|
+
provider: providers.length === 1 ? providers[0] : project.provider,
|
|
4898
|
+
providers,
|
|
4810
4899
|
entries: Object.keys(lock.entries).length,
|
|
4811
4900
|
byState,
|
|
4812
4901
|
spendByUnit: spend,
|
|
@@ -4815,6 +4904,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
4815
4904
|
} catch (err) {
|
|
4816
4905
|
projects.push({
|
|
4817
4906
|
...base,
|
|
4907
|
+
providers: [project.provider],
|
|
4818
4908
|
entries: 0,
|
|
4819
4909
|
byState: emptyStateCounts(),
|
|
4820
4910
|
spendByUnit: { generations: 0, usd: 0, free: 0 },
|
|
@@ -6411,6 +6501,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
6411
6501
|
styles: [ctx.styleId],
|
|
6412
6502
|
...orphan.width === orphan.height ? { size: orphan.width } : { width: orphan.width, height: orphan.height }
|
|
6413
6503
|
};
|
|
6504
|
+
const durableSource = shouldPersistSourceUrl(orphan.previewUrl) ? orphan.previewUrl : null;
|
|
6414
6505
|
upsert(ctx.lock, lockKey(ctx.styleId, assetId), {
|
|
6415
6506
|
styleId: ctx.styleId,
|
|
6416
6507
|
assetId,
|
|
@@ -6425,7 +6516,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
6425
6516
|
candidateIndex: null,
|
|
6426
6517
|
status: "downloaded",
|
|
6427
6518
|
error: null,
|
|
6428
|
-
|
|
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 }] : [],
|
|
6429
6523
|
outputs: [{
|
|
6430
6524
|
path: portableOutputPath(outFile, path19.dirname(ctx.manifestPath)),
|
|
6431
6525
|
sha256: sha256(buf)
|
|
@@ -6542,6 +6636,13 @@ var RecipeSchema = z5.object({
|
|
|
6542
6636
|
digest: z5.string().regex(SHA256_RE)
|
|
6543
6637
|
}).strict()
|
|
6544
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
|
+
}
|
|
6545
6646
|
const filePaths = recipe.files.map((file) => file.path);
|
|
6546
6647
|
if (new Set(filePaths).size !== filePaths.length) {
|
|
6547
6648
|
context.addIssue({ code: z5.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
|
|
@@ -6797,13 +6898,16 @@ async function installRecipe(target, options = {}) {
|
|
|
6797
6898
|
}
|
|
6798
6899
|
}
|
|
6799
6900
|
const written = await writeArtifactBundle(files);
|
|
6901
|
+
const renderedStyle = renderStyle(loaded.recipe.style, destination, cwd);
|
|
6800
6902
|
return {
|
|
6801
6903
|
recipe: loaded.recipe,
|
|
6802
6904
|
destination,
|
|
6803
6905
|
changed: written.changed,
|
|
6804
6906
|
unchanged: written.unchanged,
|
|
6805
6907
|
styleId: loaded.recipe.styleId,
|
|
6806
|
-
|
|
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 }
|
|
6807
6911
|
};
|
|
6808
6912
|
}
|
|
6809
6913
|
|
|
@@ -7399,12 +7503,34 @@ function parseArgs(argv) {
|
|
|
7399
7503
|
}
|
|
7400
7504
|
}
|
|
7401
7505
|
let budget;
|
|
7402
|
-
const
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
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) {
|
|
7406
7527
|
throw new Error(`--budget must be a non-negative number, got "${rawBudget}".`);
|
|
7407
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.");
|
|
7408
7534
|
}
|
|
7409
7535
|
const manifest = get("--manifest") ?? "pixelkiln.manifest.json";
|
|
7410
7536
|
const rawFormat = get("--format");
|
|
@@ -7437,6 +7563,7 @@ function parseArgs(argv) {
|
|
|
7437
7563
|
force: rest.includes("--force"),
|
|
7438
7564
|
yes: rest.includes("--yes") || rest.includes("-y"),
|
|
7439
7565
|
budget,
|
|
7566
|
+
providerBudgets,
|
|
7440
7567
|
dryRun: rest.includes("--dry-run"),
|
|
7441
7568
|
all: rest.includes("--all"),
|
|
7442
7569
|
json: rest.includes("--json"),
|
|
@@ -7535,7 +7662,9 @@ Options
|
|
|
7535
7662
|
--lock <path> Default: pixelkiln.lock.json beside the manifest
|
|
7536
7663
|
--style a,b Restrict to these styles
|
|
7537
7664
|
--only id1,id2 Restrict to these asset ids
|
|
7538
|
-
--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
|
|
7539
7668
|
--force Regenerate; also replace changed recipe files or quality baselines
|
|
7540
7669
|
--dry-run Never spend; doctor also skips provider connectivity
|
|
7541
7670
|
--all salvage --dry-run: list every unclaimed object, not just the first 30
|
|
@@ -7589,13 +7718,73 @@ function printPlan(plan) {
|
|
|
7589
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`
|
|
7590
7719
|
);
|
|
7591
7720
|
if (plan.actionable.length) {
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
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
|
+
}
|
|
7595
7726
|
} else {
|
|
7596
7727
|
log(` nothing to generate`);
|
|
7597
7728
|
}
|
|
7598
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
|
+
}
|
|
7599
7788
|
async function confirm(question, auto) {
|
|
7600
7789
|
if (auto) return true;
|
|
7601
7790
|
if (!process.stdin.isTTY) {
|
|
@@ -7845,7 +8034,8 @@ async function main() {
|
|
|
7845
8034
|
loadEnvFiles(path22.dirname(path22.resolve(args.manifest)));
|
|
7846
8035
|
loadEnvFiles(process.cwd());
|
|
7847
8036
|
const loaded2 = await loadManifest(args.manifest);
|
|
7848
|
-
const
|
|
8037
|
+
const selectedProvider = accountProviderId(loaded2.manifest, args.provider, "balance");
|
|
8038
|
+
const p = createProvider(selectedProvider, "online");
|
|
7849
8039
|
const b = await requireBalance(p)();
|
|
7850
8040
|
log(` provider: ${p.id}`);
|
|
7851
8041
|
log(` plan: ${b.plan ?? "n/a"}`);
|
|
@@ -8034,8 +8224,10 @@ async function main() {
|
|
|
8034
8224
|
${p.id} \u2014 ERROR: ${p.error}`);
|
|
8035
8225
|
continue;
|
|
8036
8226
|
}
|
|
8037
|
-
log(
|
|
8038
|
-
|
|
8227
|
+
log(
|
|
8228
|
+
`
|
|
8229
|
+
${p.id} (${p.providers.join(" + ")}${p.account ? `, ${p.account}` : ""})`
|
|
8230
|
+
);
|
|
8039
8231
|
log(` ${p.entries} lock entries`);
|
|
8040
8232
|
for (const [state, n] of Object.entries(p.byState)) if (n) log(` ${state.padEnd(12)} ${n}`);
|
|
8041
8233
|
for (const [unit, amount] of Object.entries(p.spendByUnit).sort()) {
|
|
@@ -8080,12 +8272,34 @@ async function main() {
|
|
|
8080
8272
|
const envFiles = [...loadEnvFiles(manifestDir)];
|
|
8081
8273
|
if (path22.resolve(process.cwd()) !== manifestDir) envFiles.push(...loadEnvFiles(process.cwd()));
|
|
8082
8274
|
const loaded = await loadManifest(args.manifest);
|
|
8083
|
-
|
|
8084
|
-
const specs = await resolveSpecs(loaded, {
|
|
8275
|
+
let specs = await resolveSpecs(loaded, {
|
|
8085
8276
|
styles: args.styles,
|
|
8086
|
-
assets: args.assets
|
|
8087
|
-
provider: estimator
|
|
8277
|
+
assets: args.assets
|
|
8088
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
|
+
}
|
|
8089
8303
|
const lock = await loadLock(args.lock);
|
|
8090
8304
|
normalizeLockOutputPaths(lock, specs);
|
|
8091
8305
|
if (args.command === "status") {
|
|
@@ -8110,14 +8324,23 @@ async function main() {
|
|
|
8110
8324
|
}
|
|
8111
8325
|
const plan = await buildPlan(specs, lock, { force: args.force });
|
|
8112
8326
|
if (args.command === "doctor") {
|
|
8113
|
-
const
|
|
8114
|
-
const
|
|
8115
|
-
|
|
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
|
+
});
|
|
8116
8341
|
const report = await doctor(loaded, specs, lock, args.lock, {
|
|
8117
|
-
|
|
8118
|
-
offline: args.dryRun
|
|
8119
|
-
apiKeyPresent,
|
|
8120
|
-
credentialEnv: factory.credentialEnv
|
|
8342
|
+
providers: providers2,
|
|
8343
|
+
offline: args.dryRun
|
|
8121
8344
|
});
|
|
8122
8345
|
if (args.json) {
|
|
8123
8346
|
log(JSON.stringify(report, null, 2));
|
|
@@ -8137,6 +8360,13 @@ async function main() {
|
|
|
8137
8360
|
cost: plan.cost,
|
|
8138
8361
|
costUnit: plan.costUnit,
|
|
8139
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
|
+
})),
|
|
8140
8370
|
actionable: plan.actionable.map((i) => i.key),
|
|
8141
8371
|
items: plan.items.map(({ key, state, reason }) => ({ key, state, reason }))
|
|
8142
8372
|
}, null, 2));
|
|
@@ -8151,7 +8381,7 @@ async function main() {
|
|
|
8151
8381
|
for (const item of plan.items) {
|
|
8152
8382
|
if (item.state !== "stale") continue;
|
|
8153
8383
|
const entry = lock.entries[item.key];
|
|
8154
|
-
if (!entry || entry.outputs.length === 0) continue;
|
|
8384
|
+
if (!entry || entry.provider !== item.spec.provider || entry.outputs.length === 0) continue;
|
|
8155
8385
|
let intact = true;
|
|
8156
8386
|
for (const output of entry.outputs) {
|
|
8157
8387
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
@@ -8417,8 +8647,18 @@ async function main() {
|
|
|
8417
8647
|
}
|
|
8418
8648
|
return;
|
|
8419
8649
|
}
|
|
8420
|
-
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
|
+
};
|
|
8421
8660
|
if (args.command === "adopt") {
|
|
8661
|
+
const provider = providerFor(selectedAccountProvider);
|
|
8422
8662
|
log(`
|
|
8423
8663
|
Reconciling account objects against files already on disk\u2026`);
|
|
8424
8664
|
const res = await adopt(provider, specs, lock, args.lock, { onProgress: log });
|
|
@@ -8453,15 +8693,16 @@ async function main() {
|
|
|
8453
8693
|
}
|
|
8454
8694
|
if (args.writePrompts) {
|
|
8455
8695
|
const { filled, stillEmpty } = await writePromptsBack(path22.resolve(args.manifest), lock, {
|
|
8456
|
-
onProgress: log
|
|
8696
|
+
onProgress: log,
|
|
8697
|
+
provider: provider.id,
|
|
8698
|
+
assetIds: specs.map((spec) => spec.assetId)
|
|
8457
8699
|
});
|
|
8458
8700
|
log(` recovered ${filled} prompt(s) into ${path22.relative(process.cwd(), args.manifest)}`);
|
|
8459
8701
|
const reloaded = await loadManifest(args.manifest);
|
|
8460
|
-
const rebased = await resolveSpecs(reloaded, {
|
|
8702
|
+
const rebased = (await resolveSpecs(reloaded, {
|
|
8461
8703
|
styles: args.styles,
|
|
8462
|
-
assets: args.assets
|
|
8463
|
-
|
|
8464
|
-
});
|
|
8704
|
+
assets: args.assets
|
|
8705
|
+
})).filter((spec) => spec.provider === provider.id);
|
|
8465
8706
|
let n = 0;
|
|
8466
8707
|
for (const spec of rebased) {
|
|
8467
8708
|
const key = lockKey(spec.styleId, spec.assetId);
|
|
@@ -8482,6 +8723,7 @@ async function main() {
|
|
|
8482
8723
|
return;
|
|
8483
8724
|
}
|
|
8484
8725
|
if (args.command === "salvage") {
|
|
8726
|
+
const provider = providerFor(selectedAccountProvider);
|
|
8485
8727
|
const jsonMode = args.dryRun && args.json;
|
|
8486
8728
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
8487
8729
|
const ownLock = path22.resolve(args.lock);
|
|
@@ -8519,7 +8761,7 @@ async function main() {
|
|
|
8519
8761
|
\`pixelkiln workspace add ${args.manifest}\` would keep it aggregated too.`
|
|
8520
8762
|
);
|
|
8521
8763
|
}
|
|
8522
|
-
const claimed = await loadClaims(lockPaths);
|
|
8764
|
+
const claimed = await loadClaims(lockPaths, { provider: provider.id });
|
|
8523
8765
|
const { orphans, total } = await findOrphans(provider, claimed, { onProgress: diag });
|
|
8524
8766
|
diag(` ${claimed.size} claimed \xB7 ${orphans.length} unclaimed of ${total}`);
|
|
8525
8767
|
if (!orphans.length) {
|
|
@@ -8533,8 +8775,8 @@ async function main() {
|
|
|
8533
8775
|
workspaceProjects.map((p) => resolveProject(workspaceDir, p).manifestPath),
|
|
8534
8776
|
args.claims
|
|
8535
8777
|
);
|
|
8536
|
-
const { matched, unmatched, elsewhere } = groupOrphansByStyle(orphans,
|
|
8537
|
-
const multiStyle = Object.keys(
|
|
8778
|
+
const { matched, unmatched, elsewhere } = groupOrphansByStyle(orphans, accountManifest, siblings);
|
|
8779
|
+
const multiStyle = Object.keys(accountManifest.styles).length > 1;
|
|
8538
8780
|
if (multiStyle) {
|
|
8539
8781
|
diag(`
|
|
8540
8782
|
by style (matched against each style's prompt prefix/suffix):`);
|
|
@@ -8574,7 +8816,7 @@ async function main() {
|
|
|
8574
8816
|
}
|
|
8575
8817
|
const rebaseline = async () => {
|
|
8576
8818
|
const reloaded = await loadManifest(args.manifest);
|
|
8577
|
-
const rebased = await resolveSpecs(reloaded
|
|
8819
|
+
const rebased = (await resolveSpecs(reloaded)).filter((spec) => spec.provider === provider.id);
|
|
8578
8820
|
let n = 0;
|
|
8579
8821
|
for (const spec of rebased) {
|
|
8580
8822
|
const key = lockKey(spec.styleId, spec.assetId);
|
|
@@ -8587,13 +8829,13 @@ async function main() {
|
|
|
8587
8829
|
if (n) log(` baselined ${n} imported asset(s) against the manifest`);
|
|
8588
8830
|
};
|
|
8589
8831
|
const runOne = async (styleId, list) => {
|
|
8590
|
-
const style =
|
|
8832
|
+
const style = accountManifest.styles[styleId];
|
|
8591
8833
|
const res = await runSalvage(
|
|
8592
8834
|
provider,
|
|
8593
8835
|
list,
|
|
8594
8836
|
{
|
|
8595
8837
|
manifestPath: loaded.path,
|
|
8596
|
-
manifest:
|
|
8838
|
+
manifest: accountManifest,
|
|
8597
8839
|
styleId,
|
|
8598
8840
|
importDir: path22.resolve(loaded.root, style.outDir),
|
|
8599
8841
|
lock,
|
|
@@ -8640,6 +8882,7 @@ async function main() {
|
|
|
8640
8882
|
return;
|
|
8641
8883
|
}
|
|
8642
8884
|
if (args.command === "purge") {
|
|
8885
|
+
const provider = providerFor(selectedAccountProvider);
|
|
8643
8886
|
const doomed = [];
|
|
8644
8887
|
for await (const obj of requireList(provider)()) {
|
|
8645
8888
|
if (obj.tags.includes("pixelkiln:discard")) {
|
|
@@ -8683,8 +8926,13 @@ async function main() {
|
|
|
8683
8926
|
return;
|
|
8684
8927
|
}
|
|
8685
8928
|
if (args.command === "tag") {
|
|
8686
|
-
|
|
8687
|
-
|
|
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)`);
|
|
8688
8936
|
return;
|
|
8689
8937
|
}
|
|
8690
8938
|
if (args.command === "submit" || args.command === "gen") {
|
|
@@ -8695,26 +8943,43 @@ async function main() {
|
|
|
8695
8943
|
return;
|
|
8696
8944
|
}
|
|
8697
8945
|
if (plan.actionable.length) {
|
|
8698
|
-
const
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
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`
|
|
8705
8971
|
);
|
|
8706
8972
|
}
|
|
8707
|
-
|
|
8973
|
+
const ceiling = budgets.get(group.provider);
|
|
8974
|
+
if (ceiling !== void 0 && group.cost > ceiling) {
|
|
8708
8975
|
throw new Error(
|
|
8709
|
-
|
|
8976
|
+
`${group.provider} would spend ${formatCost(group.costUnit, group.cost)} but its budget is ${formatCost(group.costUnit, ceiling)}.`
|
|
8710
8977
|
);
|
|
8711
8978
|
}
|
|
8712
|
-
} else {
|
|
8713
|
-
log(`
|
|
8714
|
-
${provider.id} does not expose an account balance; enforcing the explicit run budget`);
|
|
8715
8979
|
}
|
|
8980
|
+
const spendSummary = plan.groups.map((group) => `${group.provider} ${formatCost(group.costUnit, group.cost)}`).join("; ");
|
|
8716
8981
|
const ok = await confirm(
|
|
8717
|
-
` Spend ${
|
|
8982
|
+
` Spend ${spendSummary} on ${plan.actionable.length} asset(s)?`,
|
|
8718
8983
|
args.yes
|
|
8719
8984
|
);
|
|
8720
8985
|
if (!ok) {
|
|
@@ -8723,69 +8988,104 @@ async function main() {
|
|
|
8723
8988
|
}
|
|
8724
8989
|
log(`
|
|
8725
8990
|
submitting\u2026`);
|
|
8726
|
-
const
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
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
|
+
);
|
|
8743
9016
|
}
|
|
8744
|
-
|
|
8745
|
-
log(` provider balance recheck unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
9017
|
+
if (res.failed) process.exitCode = 1;
|
|
8746
9018
|
}
|
|
8747
|
-
if (res.failed) process.exitCode = 1;
|
|
8748
9019
|
}
|
|
8749
9020
|
if (args.command === "submit") return;
|
|
8750
9021
|
}
|
|
8751
9022
|
if (args.command === "poll" || args.command === "gen") {
|
|
8752
9023
|
log(`
|
|
8753
9024
|
polling\u2026`);
|
|
8754
|
-
const
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
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;
|
|
8758
9041
|
if (args.command === "poll") return;
|
|
8759
9042
|
}
|
|
8760
9043
|
if (args.command === "pick" || args.command === "gen") {
|
|
8761
|
-
const
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
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) {
|
|
8767
9056
|
log(` nothing awaiting selection`);
|
|
8768
9057
|
} else {
|
|
8769
9058
|
log(`
|
|
8770
|
-
selected ${
|
|
9059
|
+
selected ${total.selected}, left in review ${total.skipped}`);
|
|
8771
9060
|
}
|
|
8772
|
-
if (args.command === "gen" &&
|
|
9061
|
+
if (args.command === "gen" && total.skipped) process.exitCode = 1;
|
|
8773
9062
|
if (args.command === "pick") return;
|
|
8774
9063
|
}
|
|
8775
9064
|
if (args.command === "fetch" || args.command === "restore" || args.command === "gen") {
|
|
8776
9065
|
log(`
|
|
8777
9066
|
downloading\u2026`);
|
|
8778
|
-
const
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
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
|
+
}
|
|
8788
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`);
|
|
8789
9089
|
await saveLock(args.lock, lock);
|
|
8790
9090
|
log(` lockfile written: ${args.lock}`);
|
|
8791
9091
|
return;
|