pixelkiln 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts CHANGED
@@ -42,6 +42,8 @@ interface Args {
42
42
  tag: boolean;
43
43
  /** gallery: allow the page to edit manifest intent (prompts, sizes, tags, new assets). */
44
44
  edit: boolean;
45
+ /** fetch: re-download downloaded outputs and replace files whose object changed upstream. */
46
+ refresh: boolean;
45
47
  from?: string;
46
48
  out?: string;
47
49
  generator?: string;
package/dist/cli.js CHANGED
@@ -2289,6 +2289,10 @@ function lockKey(styleId, assetId) {
2289
2289
  }
2290
2290
 
2291
2291
  // src/providers/pixellab.ts
2292
+ function pixelLabObjectUrl(generator, objectId) {
2293
+ if (!objectId || generator !== "map" && generator !== "1dir") return null;
2294
+ return `https://www.pixellab.ai/objects/${encodeURIComponent(objectId)}`;
2295
+ }
2292
2296
  var PixelLabProvider = class _PixelLabProvider {
2293
2297
  constructor(client) {
2294
2298
  this.client = client;
@@ -6448,14 +6452,16 @@ function shouldPersistSourceUrl(value) {
6448
6452
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
6449
6453
  const log2 = opts.onProgress ?? (() => {
6450
6454
  });
6451
- const result = { downloaded: 0, skipped: 0, failed: 0 };
6455
+ const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
6452
6456
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
6453
6457
  normalizeLockOutputPaths(lock, specs);
6454
6458
  const cacheDir = opts.cacheDir === false ? null : path14.resolve(opts.cacheDir ?? path14.join(path14.dirname(lockPath), ".pixelkiln", "cache"));
6455
6459
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
6456
6460
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
6457
6461
  if (e.status === "selected" || e.status === "download-failed") return true;
6458
- if (!opts.repair || e.status !== "downloaded") return false;
6462
+ if (e.status !== "downloaded") return false;
6463
+ if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
6464
+ if (!opts.repair) return false;
6459
6465
  const spec = specByKey.get(key);
6460
6466
  return e.outputs.length === 0 || !spec || e.outputs.some((output) => !existsSync9(resolveOutputPath(output.path, spec.root)));
6461
6467
  });
@@ -6487,6 +6493,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
6487
6493
  continue;
6488
6494
  }
6489
6495
  const outputs = [];
6496
+ let wrote = false;
6490
6497
  try {
6491
6498
  for (let index = 0; index < sources.length; index++) {
6492
6499
  const source = sources[index];
@@ -6494,41 +6501,55 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
6494
6501
  (o) => source.role ? o.role === source.role : !o.role && sources.length === 1
6495
6502
  );
6496
6503
  const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length, source.mediaType);
6504
+ let fresh = null;
6505
+ if (opts.refresh) {
6506
+ if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
6507
+ fresh = await provider.download(source.url);
6508
+ if (recorded && sha256(fresh) === recorded.sha256) {
6509
+ log2(` current ${path14.relative(process.cwd(), target)}`);
6510
+ outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
6511
+ continue;
6512
+ }
6513
+ }
6497
6514
  if (existsSync9(target)) {
6498
6515
  const currentHash = await sha256File(target);
6499
6516
  if (recorded && currentHash === recorded.sha256) {
6500
- if (cacheDir) {
6501
- await cacheMedia(
6502
- cacheDir,
6503
- await readFile7(target),
6504
- recorded.mediaType ?? MediaType.PNG,
6505
- recorded.sha256
6506
- );
6517
+ if (!fresh) {
6518
+ if (cacheDir) {
6519
+ await cacheMedia(
6520
+ cacheDir,
6521
+ await readFile7(target),
6522
+ recorded.mediaType ?? MediaType.PNG,
6523
+ recorded.sha256
6524
+ );
6525
+ }
6526
+ outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
6527
+ continue;
6507
6528
  }
6508
- outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
6509
- continue;
6510
- }
6511
- const superseded = (entry.supersededOutputs ?? []).find(
6512
- (output, oldIndex, all) => currentOutputPath(output, spec, oldIndex, all.length) === target
6513
- );
6514
- if (!opts.force) {
6515
- if (!recorded && superseded && currentHash === superseded.sha256) {
6516
- } else if (recorded || superseded) {
6517
- throw new Error(
6518
- `refusing to overwrite modified output ${target}; pass --force to replace it`
6519
- );
6520
- } else {
6521
- throw new Error(
6522
- `refusing to overwrite untracked output ${target}; pass --force to replace it`
6523
- );
6529
+ log2(` changed ${path14.relative(process.cwd(), target)} (upstream)`);
6530
+ } else {
6531
+ const superseded = (entry.supersededOutputs ?? []).find(
6532
+ (output, oldIndex, all) => currentOutputPath(output, spec, oldIndex, all.length) === target
6533
+ );
6534
+ if (!opts.force) {
6535
+ if (!recorded && superseded && currentHash === superseded.sha256) {
6536
+ } else if (recorded || superseded) {
6537
+ throw new Error(
6538
+ `refusing to overwrite modified output ${target}; pass --force to replace it`
6539
+ );
6540
+ } else {
6541
+ throw new Error(
6542
+ `refusing to overwrite untracked output ${target}; pass --force to replace it`
6543
+ );
6544
+ }
6524
6545
  }
6546
+ log2(
6547
+ ` replace ${path14.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
6548
+ );
6525
6549
  }
6526
- log2(
6527
- ` replace ${path14.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
6528
- );
6529
6550
  }
6530
6551
  const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
6531
- let buf = recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null;
6552
+ let buf = fresh ?? (recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null);
6532
6553
  if (buf) {
6533
6554
  log2(` cached ${path14.relative(process.cwd(), target)}`);
6534
6555
  } else {
@@ -6560,8 +6581,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
6560
6581
  upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
6561
6582
  await saveLock(lockPath, lock);
6562
6583
  await rename3(tmp, target);
6584
+ wrote = true;
6563
6585
  log2(` wrote ${path14.relative(process.cwd(), target)}`);
6564
6586
  }
6587
+ if (opts.refresh && !wrote) {
6588
+ result.unchanged = (result.unchanged ?? 0) + 1;
6589
+ continue;
6590
+ }
6565
6591
  const persistentSources = sources.filter((source) => shouldPersistSourceUrl(source.url));
6566
6592
  upsert(lock, key, {
6567
6593
  status: "downloaded",
@@ -8551,6 +8577,8 @@ async function buildGallerySnapshot(opts) {
8551
8577
  source: spec.source ?? null,
8552
8578
  edit,
8553
8579
  editStatus,
8580
+ upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
8581
+ refreshable: Boolean(entry && entry.status === "downloaded" && entry.outputs.length && (entry.sourceUrls?.length || entry.sourceUrl)),
8554
8582
  tags: spec.tags,
8555
8583
  category: asset?.category ?? null
8556
8584
  });
@@ -8605,6 +8633,8 @@ async function buildGallerySnapshot(opts) {
8605
8633
  source: null,
8606
8634
  edit: null,
8607
8635
  editStatus: null,
8636
+ upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
8637
+ refreshable: false,
8608
8638
  tags: [],
8609
8639
  category: null
8610
8640
  });
@@ -9282,7 +9312,7 @@ function renderJobs() {
9282
9312
  const row = el('div', 'job');
9283
9313
  const ph = el('span', 'ph');
9284
9314
  ph.append(el('i', 'dot ' + PHASE_TONE[job.phase]), document.createTextNode(job.phase));
9285
- const what = (job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
9315
+ const what = (job.mode === 'refresh' ? 'pull upstream for ' : job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
9286
9316
  (job.project ? ' in ' + job.project : '');
9287
9317
  const last = el('span', 'last', what + (job.messages.length ? ' \u2014 ' + job.messages[job.messages.length - 1].trim() : ''));
9288
9318
  last.title = job.keys.join('\\n');
@@ -9321,13 +9351,15 @@ function budgetLine() {
9321
9351
 
9322
9352
  // The confirm step: what will be sent, what it is estimated to cost, and what
9323
9353
  // this session may still spend. Mirrors gen's "Spend \u2026 on N asset(s)?" prompt.
9324
- function generateDialog(items, { project = null, force = false, resume = false } = {}) {
9354
+ function generateDialog(items, { project = null, force = false, resume = false, refresh = false } = {}) {
9325
9355
  const host = $('dialog-host');
9326
9356
  host.textContent = '';
9327
9357
  const wrap = el('div', 'dialog');
9328
9358
  const form = el('form');
9329
- form.append(el('h3', null, resume ? 'Resume ' + items.length + (items.length === 1 ? ' asset' : ' assets')
9330
- : (force ? 'Regenerate ' : 'Generate ') + items.length + (items.length === 1 ? ' asset' : ' assets')));
9359
+ const noun = items.length + (items.length === 1 ? ' asset' : ' assets');
9360
+ form.append(el('h3', null, refresh ? 'Pull upstream changes for ' + noun
9361
+ : resume ? 'Resume ' + noun
9362
+ : (force ? 'Regenerate ' : 'Generate ') + noun));
9331
9363
  const table = el('table');
9332
9364
  const thead = el('tr'); thead.append(el('th', null, 'asset'), el('th', null, 'state'), el('th', 'n', 'candidates'), el('th', 'n', 'estimate'));
9333
9365
  table.append(thead);
@@ -9335,16 +9367,18 @@ function generateDialog(items, { project = null, force = false, resume = false }
9335
9367
  for (const item of items) {
9336
9368
  const tr = el('tr');
9337
9369
  tr.append(el('td', 'mono', item.key), el('td', null, item.state), el('td', 'n', item.candidates ?? '\u2014'),
9338
- el('td', 'n', resume ? 'no cost' : fmtCost(item.costUnit, item.estimatedCost ?? 0)));
9370
+ el('td', 'n', resume || refresh ? 'no cost' : fmtCost(item.costUnit, item.estimatedCost ?? 0)));
9339
9371
  table.append(tr);
9340
- if (!resume) {
9372
+ if (!resume && !refresh) {
9341
9373
  const g = byProvider.get(item.provider) || { cost: 0, unit: item.costUnit };
9342
9374
  g.cost += item.estimatedCost ?? 0; byProvider.set(item.provider, g);
9343
9375
  }
9344
9376
  }
9345
9377
  form.append(table);
9346
9378
  const sum = el('div', 'sum');
9347
- if (resume) {
9379
+ if (refresh) {
9380
+ sum.append(el('div', null, 'Re-downloads each object from the provider and replaces the local file only when the object changed upstream \u2014 for example after editing it in the provider\u2019s own editor. Unchanged objects are left alone; a file you changed locally is refused. Nothing is submitted.'));
9381
+ } else if (resume) {
9348
9382
  sum.append(el('div', null, 'Polls, reviews, and downloads existing provider work. Nothing is submitted.'));
9349
9383
  } else {
9350
9384
  for (const [provider, g] of byProvider) {
@@ -9361,7 +9395,7 @@ function generateDialog(items, { project = null, force = false, resume = false }
9361
9395
  }
9362
9396
  form.append(sum);
9363
9397
  const actions = el('div', 'actions');
9364
- const go = el('button', 'primary', resume ? 'Resume' : 'Generate');
9398
+ const go = el('button', 'primary', refresh ? 'Pull changes' : resume ? 'Resume' : 'Generate');
9365
9399
  go.type = 'submit';
9366
9400
  const cancel = el('button', null, 'Cancel'); cancel.type = 'button'; cancel.onclick = () => { host.textContent = ''; };
9367
9401
  const msg = el('span', 'msg');
@@ -9375,6 +9409,7 @@ function generateDialog(items, { project = null, force = false, resume = false }
9375
9409
  if (project) body.project = project;
9376
9410
  if (force) body.force = true;
9377
9411
  if (resume) body.resume = true;
9412
+ if (refresh) body.refresh = true;
9378
9413
  const job = await postGenerate(body);
9379
9414
  host.textContent = '';
9380
9415
  GEN.jobs.unshift(job);
@@ -9664,6 +9699,13 @@ function renderMain(items) {
9664
9699
  g.onclick = () => generateDialog(snap.items.filter((i) => i.project === s.project && s.actionable.keys.includes(i.key)), { project: s.project });
9665
9700
  tools.append(g);
9666
9701
  }
9702
+ const refreshable = snap.items.filter((i) => i.project === s.project && i.styleId === s.id && i.refreshable && i.upstreamUrl);
9703
+ if (GENERATION && refreshable.length) {
9704
+ const r = el('button', null, 'Pull upstream ' + refreshable.length); r.type = 'button';
9705
+ r.title = 'Re-download objects edited in the provider\u2019s own editor; no cost';
9706
+ r.onclick = () => generateDialog(refreshable, { project: s.project, refresh: true });
9707
+ tools.append(r);
9708
+ }
9667
9709
  const styleKey = 'style:' + (s.project || '') + ':' + s.id;
9668
9710
  if (EDITABLE && s.outDir) {
9669
9711
  const es = el('button', null, ui.editing === styleKey ? 'Cancel' : 'Edit style');
@@ -9757,6 +9799,30 @@ function candidatesControl(style) {
9757
9799
  return wrap;
9758
9800
  }
9759
9801
 
9802
+ function upstreamSection(item) {
9803
+ if (!item.upstreamUrl && !(GENERATION && item.refreshable)) return null;
9804
+ const s = el('section', 'meta');
9805
+ s.append(el('h3', null, 'Upstream'));
9806
+ const dl = el('dl');
9807
+ if (item.upstreamUrl) {
9808
+ const a = el('a', null, 'Open in ' + item.provider + ' \u2197');
9809
+ a.href = item.upstreamUrl; a.target = '_blank'; a.rel = 'noopener';
9810
+ row(dl, 'object', a);
9811
+ row(dl, 'note', item.provider === 'pixellab'
9812
+ ? 'The account object this generation came from. Edit it there with PixelLab\u2019s editor, then pull the changes back here; the lockfile records the new bytes as this generation.'
9813
+ : 'The provider\u2019s page for this object.');
9814
+ }
9815
+ s.append(dl);
9816
+ if (GENERATION && item.refreshable) {
9817
+ const acts = el('div', 'hand-actions');
9818
+ const b = el('button', null, 'Pull upstream changes \xB7 no cost'); b.type = 'button';
9819
+ b.onclick = () => generateDialog([item], { project: item.project, refresh: true });
9820
+ acts.append(b);
9821
+ s.append(acts);
9822
+ }
9823
+ return s;
9824
+ }
9825
+
9760
9826
  function generateActions(item) {
9761
9827
  const row = el('div', 'gen');
9762
9828
  if (!GENERATION || !item.declared || item.currentSpecHash === null) return row;
@@ -10475,6 +10541,8 @@ function renderDrawer() {
10475
10541
 
10476
10542
  const hand = handEditSection(item);
10477
10543
  if (hand) body.append(hand);
10544
+ const upstream = upstreamSection(item);
10545
+ if (upstream) body.append(upstream);
10478
10546
 
10479
10547
  if (item.revision || item.revisionParentKey) {
10480
10548
  const { s, dl } = section('Lineage');
@@ -10964,8 +11032,10 @@ var GenerateRequestSchema = z7.object({
10964
11032
  /** Regenerate work that is up to date, as `gen --force` does. */
10965
11033
  force: z7.boolean().optional(),
10966
11034
  /** Advance in-flight, review, or selected work without submitting anything. */
10967
- resume: z7.boolean().optional()
10968
- }).strict();
11035
+ resume: z7.boolean().optional(),
11036
+ /** Re-download downloaded outputs and replace files whose object changed upstream. */
11037
+ refresh: z7.boolean().optional()
11038
+ }).strict().refine((request) => !(request.resume && request.refresh), { message: "resume and refresh are exclusive" });
10969
11039
  var GenerateRequestError = class extends Error {
10970
11040
  constructor(message6, status = 400) {
10971
11041
  super(message6);
@@ -11023,6 +11093,20 @@ function createGenerateHandlers(opts) {
11023
11093
  }
11024
11094
  return groups;
11025
11095
  };
11096
+ async function refresh(job, ctx, specs) {
11097
+ job.phase = "fetching";
11098
+ for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
11099
+ const provider = opts.providerFor(job.project ?? void 0, providerId);
11100
+ const res = await fetchAssets(provider, providerSpecs, ctx.lock, ctx.lockPath, {
11101
+ onProgress: (line) => say(job, line),
11102
+ refresh: true
11103
+ });
11104
+ job.counts.downloaded += res.downloaded;
11105
+ say(job, `${providerId}: ${res.downloaded} changed upstream and replaced, ${res.unchanged ?? 0} unchanged, ${res.skipped} skipped, ${res.failed} failed`);
11106
+ }
11107
+ job.phase = "done";
11108
+ job.finishedAt = now().toISOString();
11109
+ }
11026
11110
  async function settle(job, ctx, specs) {
11027
11111
  job.phase = "polling";
11028
11112
  for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
@@ -11056,6 +11140,10 @@ function createGenerateHandlers(opts) {
11056
11140
  }
11057
11141
  async function run(job, ctx, specs, groups) {
11058
11142
  try {
11143
+ if (job.mode === "refresh") {
11144
+ await refresh(job, ctx, specs);
11145
+ return;
11146
+ }
11059
11147
  if (job.mode === "generate") {
11060
11148
  job.phase = "submitting";
11061
11149
  for (const group of groups) {
@@ -11119,7 +11207,7 @@ function createGenerateHandlers(opts) {
11119
11207
  if (holder) throw new GenerateRequestError(`"${key}" is already being worked on by job ${holder}`, 409);
11120
11208
  }
11121
11209
  let groups = [];
11122
- if (!request.resume) {
11210
+ if (!request.resume && !request.refresh) {
11123
11211
  const plan = await buildPlan(specs, ctx.lock, { force: request.force });
11124
11212
  if (!plan.actionable.length) {
11125
11213
  const reasons = plan.items.map((item) => `${item.key}: ${item.state} \u2014 ${item.reason}`).slice(0, 5);
@@ -11139,7 +11227,7 @@ function createGenerateHandlers(opts) {
11139
11227
  id: randomBytes2(8).toString("hex"),
11140
11228
  project: request.project ?? null,
11141
11229
  keys: request.keys,
11142
- mode: request.resume ? "resume" : "generate",
11230
+ mode: request.refresh ? "refresh" : request.resume ? "resume" : "generate",
11143
11231
  phase: "queued",
11144
11232
  startedAt: now().toISOString(),
11145
11233
  finishedAt: null,
@@ -11151,7 +11239,7 @@ function createGenerateHandlers(opts) {
11151
11239
  };
11152
11240
  jobs.set(job.id, job);
11153
11241
  contexts.set(job.id, ctx);
11154
- say(job, request.resume ? `resuming ${request.keys.length} asset(s) at no cost` : `generating ${groups.reduce((n, g) => n + g.actionable.length, 0)} asset(s): ` + groups.map((g) => `${g.provider} ${formatCost(g.costUnit, g.cost)}`).join("; "));
11242
+ say(job, request.refresh ? `checking ${request.keys.length} asset(s) upstream at no cost` : request.resume ? `resuming ${request.keys.length} asset(s) at no cost` : `generating ${groups.reduce((n, g) => n + g.actionable.length, 0)} asset(s): ` + groups.map((g) => `${g.provider} ${formatCost(g.costUnit, g.cost)}`).join("; "));
11155
11243
  void run(job, ctx, specs, groups);
11156
11244
  return job;
11157
11245
  },
@@ -13042,7 +13130,8 @@ var BOOL_FLAGS = [
13042
13130
  "--write-prompts",
13043
13131
  "--primary-only",
13044
13132
  "--prune",
13045
- "--edit"
13133
+ "--edit",
13134
+ "--refresh"
13046
13135
  ];
13047
13136
  var COMMANDS = [
13048
13137
  "init",
@@ -13275,6 +13364,7 @@ function parseArgs(argv) {
13275
13364
  noOpen: rest.includes("--no-open"),
13276
13365
  tag: rest.includes("--tag"),
13277
13366
  edit: rest.includes("--edit"),
13367
+ refresh: rest.includes("--refresh"),
13278
13368
  from: get("--from"),
13279
13369
  out: get("--out"),
13280
13370
  generator: get("--generator"),
@@ -13389,6 +13479,8 @@ Options
13389
13479
  --no-open Do not auto-open the browser (pick, salvage, gallery) or editor (edit)
13390
13480
  --edit gallery: allow manifest edits from the page (never spends)
13391
13481
  --tag Also push tags upstream after fetch
13482
+ --refresh fetch: re-download and replace files whose object changed
13483
+ upstream (e.g. edited in PixelLab's editor); no generation
13392
13484
  --claims a.json,b Other projects' lockfiles (salvage; required if account is shared)
13393
13485
  --workspace <path> Workspace catalog (default: pixelkiln.workspace.json). Also
13394
13486
  derives salvage's claim set instead of repeated --claims,
@@ -15033,17 +15125,19 @@ async function main() {
15033
15125
  if (args.command === "fetch" || args.command === "restore" || args.command === "gen") {
15034
15126
  log(`
15035
15127
  downloading\u2026`);
15036
- const total = { downloaded: 0, skipped: 0, failed: 0, tagged: 0 };
15128
+ const total = { downloaded: 0, skipped: 0, failed: 0, tagged: 0, unchanged: 0 };
15037
15129
  for (const [providerId, providerSpecsForRun] of specsByRecordedProvider(specs, lock)) {
15038
15130
  const groupProvider = providerFor(providerId);
15039
15131
  const res = await fetchAssets(groupProvider, providerSpecsForRun, lock, args.lock, {
15040
15132
  onProgress: log,
15041
15133
  repair: args.command === "restore",
15042
- force: args.force
15134
+ force: args.force,
15135
+ refresh: args.command === "fetch" && args.refresh
15043
15136
  });
15044
15137
  total.downloaded += res.downloaded;
15045
15138
  total.skipped += res.skipped;
15046
15139
  total.failed += res.failed;
15140
+ total.unchanged += res.unchanged ?? 0;
15047
15141
  if (args.tag) {
15048
15142
  total.tagged += await pushTags(groupProvider, providerSpecsForRun, lock, {
15049
15143
  onProgress: log
@@ -15052,7 +15146,7 @@ async function main() {
15052
15146
  }
15053
15147
  log(
15054
15148
  `
15055
- downloaded ${total.downloaded}, skipped ${total.skipped}, failed ${total.failed}`
15149
+ downloaded ${total.downloaded}, skipped ${total.skipped}, failed ${total.failed}` + (args.refresh ? `, unchanged upstream ${total.unchanged}` : "")
15056
15150
  );
15057
15151
  if (total.failed) process.exitCode = 1;
15058
15152
  if (args.tag) log(` tagged ${total.tagged} object(s) upstream`);