pixelkiln 0.63.0 → 0.64.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/README.md +2 -2
- package/dist/cli.js +252 -36
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +110 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -2
- package/dist/index.d.ts +35 -2
- package/dist/index.js +110 -17
- package/dist/index.js.map +1 -1
- package/docs/CLI.md +6 -3
- package/docs/RECOVERY.md +36 -3
- package/package.json +1 -1
- package/skills/pixelkiln/SKILL.md +2 -1
package/dist/index.d.cts
CHANGED
|
@@ -363,6 +363,12 @@ interface Provider {
|
|
|
363
363
|
listCharacters?(): AsyncGenerator<RemoteCharacter>;
|
|
364
364
|
/** One character with its rotations and animations. */
|
|
365
365
|
getCharacter?(id: string): Promise<RemoteCharacterDetail>;
|
|
366
|
+
/**
|
|
367
|
+
* Irreversible: the character and its animations. A state is its own
|
|
368
|
+
* character record and is deleted on its own. Only reached via `purge`,
|
|
369
|
+
* behind explicit confirmation.
|
|
370
|
+
*/
|
|
371
|
+
deleteCharacter?(id: string): Promise<void>;
|
|
366
372
|
/**
|
|
367
373
|
* The object's source URLs as the service hands them out now, for
|
|
368
374
|
* `fetch --refresh` when recorded URLs go stale (PixelLab stamps a
|
|
@@ -12502,6 +12508,7 @@ declare class PixelLabProvider implements Provider {
|
|
|
12502
12508
|
setTags(objectId: string, tags: string[], generator?: Generator): Promise<void>;
|
|
12503
12509
|
list(): AsyncGenerator<RemoteAsset>;
|
|
12504
12510
|
delete(assetId: string): Promise<void>;
|
|
12511
|
+
deleteCharacter(id: string): Promise<void>;
|
|
12505
12512
|
listCharacters(): AsyncGenerator<RemoteCharacter>;
|
|
12506
12513
|
getCharacter(id: string): Promise<RemoteCharacterDetail>;
|
|
12507
12514
|
/**
|
|
@@ -12835,6 +12842,7 @@ declare class FakeProvider implements Provider {
|
|
|
12835
12842
|
/** Characters answer with whatever the store holds now; other objects keep their URLs. */
|
|
12836
12843
|
refreshSources(objectId: string, context: RefreshContext): Promise<OutputSource[] | null | undefined>;
|
|
12837
12844
|
delete(assetId: string): Promise<void>;
|
|
12845
|
+
deleteCharacter(id: string): Promise<void>;
|
|
12838
12846
|
/** Seed a pre-existing remote asset, for adopt / salvage tests. */
|
|
12839
12847
|
seed(asset: Partial<RemoteAsset> & {
|
|
12840
12848
|
id: string;
|
|
@@ -18234,6 +18242,18 @@ interface Orphan {
|
|
|
18234
18242
|
createdAt: string;
|
|
18235
18243
|
previewUrl: string;
|
|
18236
18244
|
tags: string[];
|
|
18245
|
+
/**
|
|
18246
|
+
* `character` for a character group (a base and its states), triaged as
|
|
18247
|
+
* one card; absent for an ordinary account object.
|
|
18248
|
+
*/
|
|
18249
|
+
kind?: "character";
|
|
18250
|
+
/** One line the triage sheet shows under the size, e.g. a character's direction/state/loop counts. */
|
|
18251
|
+
note?: string;
|
|
18252
|
+
/** A character group's every character id, base first, with its current tags; a decision applies to all of them. */
|
|
18253
|
+
members?: {
|
|
18254
|
+
id: string;
|
|
18255
|
+
tags: string[];
|
|
18256
|
+
}[];
|
|
18237
18257
|
}
|
|
18238
18258
|
declare function findOrphans(provider: Provider, claimed: Set<string>, opts?: {
|
|
18239
18259
|
onProgress?: (msg: string) => void;
|
|
@@ -18318,6 +18338,11 @@ interface SalvageDecision {
|
|
|
18318
18338
|
*/
|
|
18319
18339
|
declare function applyTags(provider: Provider, decisions: SalvageDecision[], existing: Map<string, string[]>, opts?: {
|
|
18320
18340
|
onProgress?: (msg: string) => void;
|
|
18341
|
+
/** Character groups by their card id: the member ids and current tags a decision applies to. */
|
|
18342
|
+
characters?: Map<string, {
|
|
18343
|
+
id: string;
|
|
18344
|
+
tags: string[];
|
|
18345
|
+
}[]>;
|
|
18321
18346
|
}): Promise<{
|
|
18322
18347
|
tagged: number;
|
|
18323
18348
|
failed: number;
|
|
@@ -18328,14 +18353,22 @@ interface SalvageResult {
|
|
|
18328
18353
|
kept: number;
|
|
18329
18354
|
discarded: number;
|
|
18330
18355
|
failed: number;
|
|
18356
|
+
/**
|
|
18357
|
+
* Manifest assets written for imported character groups. They have no
|
|
18358
|
+
* lock entries yet: the caller adopts them (`adoptCharacters`) once the
|
|
18359
|
+
* manifest is saved, which downloads every rotation and frame.
|
|
18360
|
+
*/
|
|
18361
|
+
characterAssetIds?: string[];
|
|
18331
18362
|
}
|
|
18332
18363
|
/**
|
|
18333
18364
|
* Serves the triage sheet and applies decisions.
|
|
18334
18365
|
*
|
|
18335
18366
|
* `import` is the only action that touches local state: it downloads the image,
|
|
18336
18367
|
* writes a manifest asset and a lock entry, so a recovered sprite becomes a
|
|
18337
|
-
* first-class tracked asset rather than a loose file.
|
|
18338
|
-
*
|
|
18368
|
+
* first-class tracked asset rather than a loose file. A character group's
|
|
18369
|
+
* import writes its base, states, and loops as manifest assets only; the
|
|
18370
|
+
* caller adopts them. `keep` and `discard` only write tags upstream (on every
|
|
18371
|
+
* member of a character group); nothing is ever deleted here.
|
|
18339
18372
|
*/
|
|
18340
18373
|
declare function runSalvage(provider: Provider, orphans: Orphan[], ctx: {
|
|
18341
18374
|
manifestPath: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -363,6 +363,12 @@ interface Provider {
|
|
|
363
363
|
listCharacters?(): AsyncGenerator<RemoteCharacter>;
|
|
364
364
|
/** One character with its rotations and animations. */
|
|
365
365
|
getCharacter?(id: string): Promise<RemoteCharacterDetail>;
|
|
366
|
+
/**
|
|
367
|
+
* Irreversible: the character and its animations. A state is its own
|
|
368
|
+
* character record and is deleted on its own. Only reached via `purge`,
|
|
369
|
+
* behind explicit confirmation.
|
|
370
|
+
*/
|
|
371
|
+
deleteCharacter?(id: string): Promise<void>;
|
|
366
372
|
/**
|
|
367
373
|
* The object's source URLs as the service hands them out now, for
|
|
368
374
|
* `fetch --refresh` when recorded URLs go stale (PixelLab stamps a
|
|
@@ -12502,6 +12508,7 @@ declare class PixelLabProvider implements Provider {
|
|
|
12502
12508
|
setTags(objectId: string, tags: string[], generator?: Generator): Promise<void>;
|
|
12503
12509
|
list(): AsyncGenerator<RemoteAsset>;
|
|
12504
12510
|
delete(assetId: string): Promise<void>;
|
|
12511
|
+
deleteCharacter(id: string): Promise<void>;
|
|
12505
12512
|
listCharacters(): AsyncGenerator<RemoteCharacter>;
|
|
12506
12513
|
getCharacter(id: string): Promise<RemoteCharacterDetail>;
|
|
12507
12514
|
/**
|
|
@@ -12835,6 +12842,7 @@ declare class FakeProvider implements Provider {
|
|
|
12835
12842
|
/** Characters answer with whatever the store holds now; other objects keep their URLs. */
|
|
12836
12843
|
refreshSources(objectId: string, context: RefreshContext): Promise<OutputSource[] | null | undefined>;
|
|
12837
12844
|
delete(assetId: string): Promise<void>;
|
|
12845
|
+
deleteCharacter(id: string): Promise<void>;
|
|
12838
12846
|
/** Seed a pre-existing remote asset, for adopt / salvage tests. */
|
|
12839
12847
|
seed(asset: Partial<RemoteAsset> & {
|
|
12840
12848
|
id: string;
|
|
@@ -18234,6 +18242,18 @@ interface Orphan {
|
|
|
18234
18242
|
createdAt: string;
|
|
18235
18243
|
previewUrl: string;
|
|
18236
18244
|
tags: string[];
|
|
18245
|
+
/**
|
|
18246
|
+
* `character` for a character group (a base and its states), triaged as
|
|
18247
|
+
* one card; absent for an ordinary account object.
|
|
18248
|
+
*/
|
|
18249
|
+
kind?: "character";
|
|
18250
|
+
/** One line the triage sheet shows under the size, e.g. a character's direction/state/loop counts. */
|
|
18251
|
+
note?: string;
|
|
18252
|
+
/** A character group's every character id, base first, with its current tags; a decision applies to all of them. */
|
|
18253
|
+
members?: {
|
|
18254
|
+
id: string;
|
|
18255
|
+
tags: string[];
|
|
18256
|
+
}[];
|
|
18237
18257
|
}
|
|
18238
18258
|
declare function findOrphans(provider: Provider, claimed: Set<string>, opts?: {
|
|
18239
18259
|
onProgress?: (msg: string) => void;
|
|
@@ -18318,6 +18338,11 @@ interface SalvageDecision {
|
|
|
18318
18338
|
*/
|
|
18319
18339
|
declare function applyTags(provider: Provider, decisions: SalvageDecision[], existing: Map<string, string[]>, opts?: {
|
|
18320
18340
|
onProgress?: (msg: string) => void;
|
|
18341
|
+
/** Character groups by their card id: the member ids and current tags a decision applies to. */
|
|
18342
|
+
characters?: Map<string, {
|
|
18343
|
+
id: string;
|
|
18344
|
+
tags: string[];
|
|
18345
|
+
}[]>;
|
|
18321
18346
|
}): Promise<{
|
|
18322
18347
|
tagged: number;
|
|
18323
18348
|
failed: number;
|
|
@@ -18328,14 +18353,22 @@ interface SalvageResult {
|
|
|
18328
18353
|
kept: number;
|
|
18329
18354
|
discarded: number;
|
|
18330
18355
|
failed: number;
|
|
18356
|
+
/**
|
|
18357
|
+
* Manifest assets written for imported character groups. They have no
|
|
18358
|
+
* lock entries yet: the caller adopts them (`adoptCharacters`) once the
|
|
18359
|
+
* manifest is saved, which downloads every rotation and frame.
|
|
18360
|
+
*/
|
|
18361
|
+
characterAssetIds?: string[];
|
|
18331
18362
|
}
|
|
18332
18363
|
/**
|
|
18333
18364
|
* Serves the triage sheet and applies decisions.
|
|
18334
18365
|
*
|
|
18335
18366
|
* `import` is the only action that touches local state: it downloads the image,
|
|
18336
18367
|
* writes a manifest asset and a lock entry, so a recovered sprite becomes a
|
|
18337
|
-
* first-class tracked asset rather than a loose file.
|
|
18338
|
-
*
|
|
18368
|
+
* first-class tracked asset rather than a loose file. A character group's
|
|
18369
|
+
* import writes its base, states, and loops as manifest assets only; the
|
|
18370
|
+
* caller adopts them. `keep` and `discard` only write tags upstream (on every
|
|
18371
|
+
* member of a character group); nothing is ever deleted here.
|
|
18339
18372
|
*/
|
|
18340
18373
|
declare function runSalvage(provider: Provider, orphans: Orphan[], ctx: {
|
|
18341
18374
|
manifestPath: string;
|
package/dist/index.js
CHANGED
|
@@ -4734,8 +4734,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4734
4734
|
const sources = [];
|
|
4735
4735
|
const terrainTiles = [];
|
|
4736
4736
|
for (const [index, tile] of tileset.tiles.entries()) {
|
|
4737
|
-
const
|
|
4738
|
-
const role = `tile-${String(index).padStart(2, "0")}-${
|
|
4737
|
+
const slug2 = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
|
|
4738
|
+
const role = `tile-${String(index).padStart(2, "0")}-${slug2}`;
|
|
4739
4739
|
const file = path.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
|
|
4740
4740
|
writeFileSync(file, Buffer.from(tile.image.base64, "base64"));
|
|
4741
4741
|
sources.push({ url: `file://${file}`, role });
|
|
@@ -4854,6 +4854,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4854
4854
|
async delete(assetId) {
|
|
4855
4855
|
await this.client.deleteObject(assetId);
|
|
4856
4856
|
}
|
|
4857
|
+
async deleteCharacter(id) {
|
|
4858
|
+
await this.client.deleteCharacter(id);
|
|
4859
|
+
}
|
|
4857
4860
|
async *listCharacters() {
|
|
4858
4861
|
for await (const character of this.client.iterateCharacters(100)) {
|
|
4859
4862
|
yield remoteCharacter(character);
|
|
@@ -6877,6 +6880,8 @@ var FakeProvider = class {
|
|
|
6877
6880
|
this.tags.set(objectId, [...tags]);
|
|
6878
6881
|
const asset = this.assets.get(objectId);
|
|
6879
6882
|
if (asset) asset.tags = [...tags];
|
|
6883
|
+
const character = this.characters.get(objectId.split("#")[0]);
|
|
6884
|
+
if (character) character.tags = [...tags];
|
|
6880
6885
|
}
|
|
6881
6886
|
/**
|
|
6882
6887
|
* Assigned in the constructor rather than declared as a method, so a test can
|
|
@@ -6911,6 +6916,10 @@ var FakeProvider = class {
|
|
|
6911
6916
|
this.deleted.push(assetId);
|
|
6912
6917
|
this.assets.delete(assetId);
|
|
6913
6918
|
}
|
|
6919
|
+
async deleteCharacter(id) {
|
|
6920
|
+
this.deleted.push(id);
|
|
6921
|
+
this.characters.delete(id);
|
|
6922
|
+
}
|
|
6914
6923
|
/** Seed a pre-existing remote asset, for adopt / salvage tests. */
|
|
6915
6924
|
seed(asset) {
|
|
6916
6925
|
const full = {
|
|
@@ -16435,6 +16444,9 @@ async function loadClaims(lockPaths, opts = {}) {
|
|
|
16435
16444
|
add(entry.objectId);
|
|
16436
16445
|
add(entry.reviewObjectId);
|
|
16437
16446
|
add(entry.jobId);
|
|
16447
|
+
for (const id of [entry.objectId, entry.jobId]) {
|
|
16448
|
+
if (id?.includes("#")) add(id.split("#")[0]);
|
|
16449
|
+
}
|
|
16438
16450
|
}
|
|
16439
16451
|
}
|
|
16440
16452
|
return claimed;
|
|
@@ -16458,6 +16470,54 @@ async function findOrphans(provider, claimed, opts = {}) {
|
|
|
16458
16470
|
})).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
16459
16471
|
return { orphans, total: all.length };
|
|
16460
16472
|
}
|
|
16473
|
+
function isState(character) {
|
|
16474
|
+
return Boolean(character.stateName && character.stateName !== "Idle");
|
|
16475
|
+
}
|
|
16476
|
+
function characterSalvageAssets(characters, ctx) {
|
|
16477
|
+
const ordered = [...characters].sort((a, b) => Number(isState(a)) - Number(isState(b)) || a.createdAt.localeCompare(b.createdAt));
|
|
16478
|
+
const base = ordered[0];
|
|
16479
|
+
const common = { styles: [ctx.styleId], tags: ["salvaged"] };
|
|
16480
|
+
const assets = {};
|
|
16481
|
+
const skipped = [];
|
|
16482
|
+
const baseId = idFromPrompt(base.name || base.prompt, ctx.taken);
|
|
16483
|
+
const size = base.width === base.height && base.width >= 32 && base.width <= 256 && base.width !== ctx.styleSize ? { size: base.width } : {};
|
|
16484
|
+
assets[baseId] = { prompt: base.prompt || base.name || baseId, remoteId: base.id, ...size, ...common };
|
|
16485
|
+
const assetIdOf = /* @__PURE__ */ new Map([[base.id, baseId]]);
|
|
16486
|
+
for (const state of ordered.slice(1)) {
|
|
16487
|
+
const stateId = uniqueId(`${baseId}_${slug(state.stateName ?? "state")}`, ctx.taken);
|
|
16488
|
+
assetIdOf.set(state.id, stateId);
|
|
16489
|
+
assets[stateId] = { prompt: state.prompt || state.stateName || stateId, state: { of: baseId }, remoteId: state.id, ...common };
|
|
16490
|
+
}
|
|
16491
|
+
for (const character of ordered) {
|
|
16492
|
+
const parentId = assetIdOf.get(character.id);
|
|
16493
|
+
for (const animation of character.animations) {
|
|
16494
|
+
if (!animation.frames.length) continue;
|
|
16495
|
+
const label = animation.name || animation.type;
|
|
16496
|
+
if (!animation.groupId) {
|
|
16497
|
+
skipped.push(`${parentId}: "${label}" ${animation.direction} loop has no animation group id to adopt it by`);
|
|
16498
|
+
continue;
|
|
16499
|
+
}
|
|
16500
|
+
const loopId = uniqueId(`${parentId}.${slug(label)}.${animation.direction}`, ctx.taken);
|
|
16501
|
+
assets[loopId] = {
|
|
16502
|
+
prompt: label,
|
|
16503
|
+
animation: { of: parentId, direction: animation.direction },
|
|
16504
|
+
remoteId: `${character.id}#${animation.groupId}`,
|
|
16505
|
+
...common
|
|
16506
|
+
};
|
|
16507
|
+
}
|
|
16508
|
+
}
|
|
16509
|
+
return { assets, skipped };
|
|
16510
|
+
}
|
|
16511
|
+
function slug(text) {
|
|
16512
|
+
return text.toLowerCase().replace(/^pixelkiln:.*\//, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "loop";
|
|
16513
|
+
}
|
|
16514
|
+
function uniqueId(base, taken) {
|
|
16515
|
+
let id = base;
|
|
16516
|
+
let n = 2;
|
|
16517
|
+
while (taken.has(id)) id = `${base}_${n++}`;
|
|
16518
|
+
taken.add(id);
|
|
16519
|
+
return id;
|
|
16520
|
+
}
|
|
16461
16521
|
var MIN_STYLE_FRAGMENT_LENGTH = 12;
|
|
16462
16522
|
function matchStyleByPattern(prompt, manifest) {
|
|
16463
16523
|
const p = prompt.toLowerCase();
|
|
@@ -16587,18 +16647,22 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
16587
16647
|
let tagged = 0;
|
|
16588
16648
|
let failed = 0;
|
|
16589
16649
|
for (const { id, action } of decisions) {
|
|
16590
|
-
const
|
|
16591
|
-
|
|
16592
|
-
)
|
|
16593
|
-
|
|
16594
|
-
|
|
16595
|
-
|
|
16596
|
-
|
|
16597
|
-
|
|
16598
|
-
tagged
|
|
16599
|
-
|
|
16600
|
-
|
|
16601
|
-
|
|
16650
|
+
const group = opts.characters?.get(id);
|
|
16651
|
+
const targets = group ?? [{ id, tags: existing.get(id) ?? [] }];
|
|
16652
|
+
for (const target of targets) {
|
|
16653
|
+
const current = target.tags.filter(
|
|
16654
|
+
(t) => t !== "pixelkiln:keep" && t !== "pixelkiln:discard" && t !== "pixelkiln:imported"
|
|
16655
|
+
);
|
|
16656
|
+
const decisionTag = `pixelkiln:${action === "import" ? "imported" : action}`;
|
|
16657
|
+
const next = [...current.slice(0, 19), decisionTag];
|
|
16658
|
+
if (!provider.setTags) return { tagged, failed };
|
|
16659
|
+
try {
|
|
16660
|
+
await provider.setTags(target.id, next, group ? "character" : void 0);
|
|
16661
|
+
tagged++;
|
|
16662
|
+
} catch (err) {
|
|
16663
|
+
failed++;
|
|
16664
|
+
log(` tag failed ${target.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
16665
|
+
}
|
|
16602
16666
|
}
|
|
16603
16667
|
}
|
|
16604
16668
|
return { tagged, failed };
|
|
@@ -16611,7 +16675,7 @@ import path33 from "path";
|
|
|
16611
16675
|
// src/pick/salvage-sheet.ts
|
|
16612
16676
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
16613
16677
|
function renderSalvageSheet(orphans, ctx) {
|
|
16614
|
-
const safe = orphans.map((o) => ({ ...o, prompt: escapeHtml3(o.prompt) }));
|
|
16678
|
+
const safe = orphans.map((o) => ({ ...o, prompt: escapeHtml3(o.prompt), ...o.note ? { note: escapeHtml3(o.note) } : {} }));
|
|
16615
16679
|
const data = JSON.stringify(safe).replace(/<\//g, "<\\/");
|
|
16616
16680
|
const styleId = ctx ? escapeHtml3(ctx.styleId) : null;
|
|
16617
16681
|
const importDir = ctx ? escapeHtml3(ctx.importDir) : null;
|
|
@@ -16697,6 +16761,7 @@ ITEMS.forEach((o, i) => {
|
|
|
16697
16761
|
el.innerHTML =
|
|
16698
16762
|
'<div class="thumb"><img src="' + o.previewUrl + '" loading="lazy"></div>' +
|
|
16699
16763
|
'<div class="meta">' + o.width + '\xD7' + o.height + ' \xB7 ' + o.createdAt.slice(0,10) + '</div>' +
|
|
16764
|
+
(o.note ? '<div class="meta">' + o.note + '</div>' : '') +
|
|
16700
16765
|
'<div class="prompt">' + o.prompt + '</div>' +
|
|
16701
16766
|
'<div class="acts">' +
|
|
16702
16767
|
'<button data-a="import">import</button>' +
|
|
@@ -16775,6 +16840,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
16775
16840
|
});
|
|
16776
16841
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
16777
16842
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
16843
|
+
const characterMembers = new Map(
|
|
16844
|
+
orphans.flatMap((o) => o.kind === "character" && o.members ? [[o.id, o.members]] : [])
|
|
16845
|
+
);
|
|
16778
16846
|
return serveReviewPage({
|
|
16779
16847
|
html,
|
|
16780
16848
|
port: opts.port,
|
|
@@ -16791,10 +16859,34 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
16791
16859
|
const result = { imported: 0, kept: 0, discarded: 0, failed: 0 };
|
|
16792
16860
|
const taken = new Set(Object.keys(ctx.manifest.assets));
|
|
16793
16861
|
const importedAssetIds = [];
|
|
16862
|
+
const characterAssetIds = [];
|
|
16794
16863
|
for (const decision of decisions) {
|
|
16795
16864
|
const orphan = byId.get(decision.id);
|
|
16796
16865
|
if (!orphan) continue;
|
|
16797
|
-
if (decision.action === "import") {
|
|
16866
|
+
if (decision.action === "import" && orphan.kind === "character") {
|
|
16867
|
+
try {
|
|
16868
|
+
if (!provider.getCharacter) throw new Error(`provider ${provider.id} cannot read characters`);
|
|
16869
|
+
const details = [];
|
|
16870
|
+
for (const member of orphan.members ?? [{ id: orphan.id }]) details.push(await provider.getCharacter(member.id));
|
|
16871
|
+
const style = ctx.manifest.styles[ctx.styleId];
|
|
16872
|
+
const { assets, skipped } = characterSalvageAssets(details, {
|
|
16873
|
+
styleId: ctx.styleId,
|
|
16874
|
+
styleSize: style?.size,
|
|
16875
|
+
taken
|
|
16876
|
+
});
|
|
16877
|
+
for (const [assetId, asset] of Object.entries(assets)) {
|
|
16878
|
+
ctx.manifest.assets[assetId] = asset;
|
|
16879
|
+
importedAssetIds.push(assetId);
|
|
16880
|
+
characterAssetIds.push(assetId);
|
|
16881
|
+
}
|
|
16882
|
+
for (const line of skipped) log(` not imported: ${line}`);
|
|
16883
|
+
result.imported++;
|
|
16884
|
+
log(` imported character ${orphan.id} as ${Object.keys(assets).length} asset(s)`);
|
|
16885
|
+
} catch (err) {
|
|
16886
|
+
result.failed++;
|
|
16887
|
+
log(` import failed ${orphan.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
16888
|
+
}
|
|
16889
|
+
} else if (decision.action === "import") {
|
|
16798
16890
|
try {
|
|
16799
16891
|
const buf = await provider.download(orphan.previewUrl);
|
|
16800
16892
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
@@ -16858,11 +16950,12 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
16858
16950
|
result.discarded++;
|
|
16859
16951
|
}
|
|
16860
16952
|
}
|
|
16861
|
-
await applyTags(provider, decisions, existingTags, { onProgress: log });
|
|
16953
|
+
await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
|
|
16862
16954
|
const raw = JSON.parse(await readFile26(ctx.manifestPath, "utf8"));
|
|
16863
16955
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
16864
16956
|
await writeFile13(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
16865
16957
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
16958
|
+
if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
|
|
16866
16959
|
return result;
|
|
16867
16960
|
}
|
|
16868
16961
|
});
|