fluncle 0.89.0 → 0.91.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.
Files changed (2) hide show
  1. package/bin/fluncle.mjs +78 -12
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -497,14 +497,32 @@ var init_util = __esm(() => {
497
497
  });
498
498
 
499
499
  // src/output.ts
500
+ import { writeSync as writeSync2 } from "node:fs";
501
+ function writeStdoutSync2(text) {
502
+ const buf = Buffer.from(text, "utf8");
503
+ let offset = 0;
504
+ while (offset < buf.length) {
505
+ try {
506
+ offset += writeSync2(1, buf, offset);
507
+ } catch (error) {
508
+ if (error.code === "EAGAIN") {
509
+ Atomics.wait(EAGAIN_WAIT2, 0, 0, 1);
510
+ continue;
511
+ }
512
+ throw error;
513
+ }
514
+ }
515
+ }
500
516
  function isJsonFailure(value) {
501
517
  return typeof value === "object" && value !== null && typeof value.code === "string" && typeof value.message === "string";
502
518
  }
503
519
  function printJson2(value) {
504
- console.log(JSON.stringify(value, null, 2));
520
+ writeStdoutSync2(JSON.stringify(value, null, 2) + `
521
+ `);
505
522
  }
506
- var CliError2;
523
+ var EAGAIN_WAIT2, CliError2;
507
524
  var init_output = __esm(() => {
525
+ EAGAIN_WAIT2 = new Int32Array(new SharedArrayBuffer(4));
508
526
  CliError2 = class CliError2 extends Error {
509
527
  code;
510
528
  constructor(code, message) {
@@ -538,7 +556,7 @@ function parseVersion(version) {
538
556
  var currentVersion;
539
557
  var init_version = __esm(() => {
540
558
  init_output();
541
- currentVersion = "0.89.0".trim() ? "0.89.0".trim() : "0.1.0";
559
+ currentVersion = "0.91.0".trim() ? "0.91.0".trim() : "0.1.0";
542
560
  });
543
561
 
544
562
  // src/update-notifier.ts
@@ -2168,7 +2186,7 @@ function parseVersion2(version) {
2168
2186
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2169
2187
  var init_version2 = __esm(() => {
2170
2188
  init_output();
2171
- currentVersion2 = "0.89.0".trim() ? "0.89.0".trim() : "0.1.0";
2189
+ currentVersion2 = "0.91.0".trim() ? "0.91.0".trim() : "0.1.0";
2172
2190
  });
2173
2191
 
2174
2192
  // ../../packages/registry/src/index.ts
@@ -2895,17 +2913,30 @@ __export(exports_track, {
2895
2913
  trackGetCommand: () => trackGetCommand,
2896
2914
  trackDraftCommand: () => trackDraftCommand,
2897
2915
  trackContextCommand: () => trackContextCommand,
2916
+ isPlatesOnlyUpload: () => isPlatesOnlyUpload,
2898
2917
  checkBundleCompleteness: () => checkBundleCompleteness
2899
2918
  });
2900
2919
  async function trackGetCommand(idOrLogId) {
2901
2920
  return publicApiGet(`/api/tracks/${encodeURIComponent(idOrLogId)}`);
2902
2921
  }
2922
+ function isPlatesOnlyUpload(files) {
2923
+ const hasPlate = PLATE_FIELDS.some((option) => Boolean(files[option]));
2924
+ if (!hasPlate) {
2925
+ return false;
2926
+ }
2927
+ return Object.keys(files).every((option) => !files[option] || PLATE_FIELDS.includes(option) || NON_FILE_OPTIONS.includes(option));
2928
+ }
2903
2929
  function checkBundleCompleteness(files) {
2904
2930
  const uploadingFootage = FOOTAGE_FIELDS.some((option) => Boolean(files[option]));
2905
2931
  const missingFrom = (specs) => uploadingFootage ? specs.filter((spec) => !files[spec.option]).map((spec) => spec.file) : [];
2932
+ const plateWarnings = [];
2933
+ if (files.plateBackground && !files.plate) {
2934
+ plateWarnings.push("plate.background.png without plate.png — the background is the parallax layer OF a plate; pass --plate (or drop it in the --dir) too");
2935
+ }
2906
2936
  return {
2907
2937
  missingAdvisory: missingFrom(RERENDER_ADVISORY_FIELDS),
2908
2938
  missingContract: missingFrom(RERENDER_CONTRACT_FIELDS),
2939
+ plateWarnings,
2909
2940
  uploadingFootage
2910
2941
  };
2911
2942
  }
@@ -2922,6 +2953,9 @@ async function trackVideoCommand(idOrLogId, files, onProgress, options = {}) {
2922
2953
  onProgress?.(`warning: ${missing} missing (provenance/eval only) — shipping without it`);
2923
2954
  }
2924
2955
  }
2956
+ for (const warning of completeness.plateWarnings) {
2957
+ onProgress?.(`warning: ${warning}`);
2958
+ }
2925
2959
  const present = VIDEO_FIELDS.map((spec) => ({
2926
2960
  field: spec.field,
2927
2961
  path: files[spec.option]
@@ -2929,6 +2963,7 @@ async function trackVideoCommand(idOrLogId, files, onProgress, options = {}) {
2929
2963
  if (present.length === 0) {
2930
2964
  throw new CliError2("nothing_to_upload", "No bundle files resolved to upload (pass --dir <bundle> or explicit file flags).");
2931
2965
  }
2966
+ const platesOnly = isPlatesOnlyUpload(files);
2932
2967
  const presign = await adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/video/uploads`, { fields: present.map((spec) => spec.field) });
2933
2968
  const byField = new Map(presign.uploads.map((upload) => [upload.field, upload]));
2934
2969
  const urls = {};
@@ -2949,6 +2984,10 @@ async function trackVideoCommand(idOrLogId, files, onProgress, options = {}) {
2949
2984
  }
2950
2985
  urls[spec.field] = `${FOUND_BASE}/${upload.key}`;
2951
2986
  }
2987
+ if (platesOnly) {
2988
+ onProgress?.(`plate pre-upload complete — compose against ${FOUND_BASE}/${presign.logId}/plate.png; finalize is deferred to the footage ship`);
2989
+ return { logId: presign.logId, ok: true, trackId: presign.trackId, urls };
2990
+ }
2952
2991
  const manifest = files.render ? await readManifestFields(files.render) : {};
2953
2992
  const videoModel = files.model?.trim().slice(0, 120) || manifest.model || DEFAULT_VIDEO_MODEL;
2954
2993
  const videoModelReasoning = files.reasoning?.trim().slice(0, 120) || manifest.reasoning || DEFAULT_VIDEO_REASONING;
@@ -3058,7 +3097,7 @@ async function trackNoteCommand(idOrLogId, options) {
3058
3097
  const body = { note: options.note };
3059
3098
  return adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/note`, body);
3060
3099
  }
3061
- var DEFAULT_VIDEO_MODEL = "anthropic/claude-opus-4-8", DEFAULT_VIDEO_REASONING = "high", FOUND_BASE = "https://found.fluncle.com", VIDEO_FIELDS, RERENDER_CONTRACT_FIELDS, RERENDER_ADVISORY_FIELDS, FOOTAGE_FIELDS;
3100
+ var DEFAULT_VIDEO_MODEL = "anthropic/claude-opus-4-8", DEFAULT_VIDEO_REASONING = "high", FOUND_BASE = "https://found.fluncle.com", VIDEO_FIELDS, RERENDER_CONTRACT_FIELDS, RERENDER_ADVISORY_FIELDS, FOOTAGE_FIELDS, PLATE_FIELDS, NON_FILE_OPTIONS;
3062
3101
  var init_track = __esm(() => {
3063
3102
  init_api();
3064
3103
  init_output();
@@ -3070,6 +3109,8 @@ var init_track = __esm(() => {
3070
3109
  { field: "footage-landscape-social", option: "footageLandscapeSocial" },
3071
3110
  { field: "poster", option: "poster" },
3072
3111
  { field: "cover", option: "cover" },
3112
+ { field: "plate", option: "plate" },
3113
+ { field: "plate-background", option: "plateBackground" },
3073
3114
  { field: "note", option: "note" },
3074
3115
  { field: "composition", option: "composition" },
3075
3116
  { field: "props", option: "props" },
@@ -3095,6 +3136,8 @@ var init_track = __esm(() => {
3095
3136
  "footageLandscape",
3096
3137
  "footageLandscapeSocial"
3097
3138
  ];
3139
+ PLATE_FIELDS = ["plate", "plateBackground"];
3140
+ NON_FILE_OPTIONS = ["model", "reasoning"];
3098
3141
  });
3099
3142
 
3100
3143
  // src/commands/add.ts
@@ -7081,6 +7124,23 @@ var spotifyPlaylistUrl = "https://open.spotify.com/playlist/1m5LADqpLjiBERdtqrIi
7081
7124
  var telegramUrl = "https://t.me/fluncle";
7082
7125
 
7083
7126
  // src/output.ts
7127
+ import { writeSync } from "node:fs";
7128
+ var EAGAIN_WAIT = new Int32Array(new SharedArrayBuffer(4));
7129
+ function writeStdoutSync(text) {
7130
+ const buf = Buffer.from(text, "utf8");
7131
+ let offset = 0;
7132
+ while (offset < buf.length) {
7133
+ try {
7134
+ offset += writeSync(1, buf, offset);
7135
+ } catch (error) {
7136
+ if (error.code === "EAGAIN") {
7137
+ Atomics.wait(EAGAIN_WAIT, 0, 0, 1);
7138
+ continue;
7139
+ }
7140
+ throw error;
7141
+ }
7142
+ }
7143
+ }
7084
7144
  class CliError extends Error {
7085
7145
  code;
7086
7146
  constructor(code, message) {
@@ -7090,7 +7150,8 @@ class CliError extends Error {
7090
7150
  }
7091
7151
  }
7092
7152
  function printJson(value) {
7093
- console.log(JSON.stringify(value, null, 2));
7153
+ writeStdoutSync(JSON.stringify(value, null, 2) + `
7154
+ `);
7094
7155
  }
7095
7156
  function toJsonFailure(error) {
7096
7157
  if (error instanceof CliError) {
@@ -7250,7 +7311,7 @@ function addAdminCommands(program2) {
7250
7311
  const { queueCommand: queueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
7251
7312
  await runAdminQueue(options, queueCommand2);
7252
7313
  });
7253
- adminTracks.command("list").description("List findings, filterable by musical-key presence (--no-key / --has-key)").option("--limit <limit>", "Number of findings to show", "50").option("--no-key", "Only findings with NO stored musical key (the key-backfill backlog)").option("--has-key <bool>", "Filter by key presence: true (has key) or false (missing)").option("--order <order>", "Sort: asc (oldest first) or desc (newest first)", "desc").option("--json", "Print JSON", false).action(async (options) => {
7314
+ adminTracks.command("list").description("List findings, filterable by musical-key presence (--no-key / --has-key)").option("--limit <limit>", "Number of findings to show (1-100)", "50").option("--all", "Fetch the ENTIRE catalogue, paginating past the 100-row cap (overrides --limit)", false).option("--no-key", "Only findings with NO stored musical key (the key-backfill backlog)").option("--has-key <bool>", "Filter by key presence: true (has key) or false (missing)").option("--order <order>", "Sort: asc (oldest first) or desc (newest first)", "desc").option("--json", "Print JSON", false).action(async (options) => {
7254
7315
  const { listCommand: listCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
7255
7316
  await runAdminTracksList(options, listCommand2);
7256
7317
  });
@@ -7272,7 +7333,7 @@ function addAdminCommands(program2) {
7272
7333
  const { trackUpdateCommand: trackUpdateCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
7273
7334
  await runTrackUpdate(trackId, options, trackUpdateCommand2);
7274
7335
  });
7275
- adminTrack.command("video").description("Upload a track's video bundle to R2 and link it").argument("[idOrLogId]").option("--allow-partial", "Allow an intentionally partial upload (skip the re-render-contract check; e.g. poster-only)", false).option("--composition <file>", "Composition source file").option("--cover <file>", "Cover image").option("--dir <dir>", "Bundle directory").option("--footage <file>", "Video footage (square crop source)").option("--footage-landscape <file>", "Clean landscape cut (optional escape hatch)").option("--footage-landscape-social <file>", "Landscape cut with baked text (optional)").option("--footage-notext <file>", "Portrait cut without the type layer (optional)").option("--footage-social <file>", "Portrait social cut (baked text)").option("--intent <file>", "Render-intent JSON (optional)").option("--json", "Print JSON", false).option("--metrics <file>", "Gate metrics JSON (optional)").option("--model <model>", "Authoring AI model (<provider>/<model>)").option("--note <file>", "Note file").option("--poster <file>", "Poster image").option("--props <file>", "Render props JSON").option("--reasoning <level>", "Authoring model reasoning effort (e.g. high)").option("--render <file>", "Render metadata JSON").option("--scene <file>", "Scene replay manifest JSON (fluncle.scene/1, optional)").allowExcessArguments().action(async (idOrLogId, options) => {
7336
+ adminTrack.command("video").description("Upload a track's video bundle to R2 and link it").argument("[idOrLogId]").option("--allow-partial", "Allow an intentionally partial upload (skip the re-render-contract check; e.g. poster-only)", false).option("--composition <file>", "Composition source file").option("--cover <file>", "Cover image").option("--dir <dir>", "Bundle directory").option("--footage <file>", "Video footage (square crop source)").option("--footage-landscape <file>", "Clean landscape cut (optional escape hatch)").option("--footage-landscape-social <file>", "Landscape cut with baked text (optional)").option("--footage-notext <file>", "Portrait cut without the type layer (optional)").option("--footage-social <file>", "Portrait social cut (baked text)").option("--intent <file>", "Render-intent JSON (optional)").option("--json", "Print JSON", false).option("--metrics <file>", "Gate metrics JSON (optional)").option("--model <model>", "Authoring AI model (<provider>/<model>)").option("--note <file>", "Note file").option("--plate <file>", "Plate-lane photographic plate (plate.png; uploadable pre-render)").option("--plate-background <file>", "The plate's subject-removed background (plate.background.png, optional)").option("--poster <file>", "Poster image").option("--props <file>", "Render props JSON").option("--reasoning <level>", "Authoring model reasoning effort (e.g. high)").option("--render <file>", "Render metadata JSON").option("--scene <file>", "Scene replay manifest JSON (fluncle.scene/1, optional)").allowExcessArguments().action(async (idOrLogId, options) => {
7276
7337
  const { trackVideoCommand: trackVideoCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
7277
7338
  await runTrackVideo(idOrLogId, options, trackVideoCommand2);
7278
7339
  });
@@ -7697,7 +7758,7 @@ async function runBackfillDiscogs(options, backfillDiscogsCommand2) {
7697
7758
  }
7698
7759
  async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
7699
7760
  if (!idOrLogId) {
7700
- throw new Error("Missing id. Usage: fluncle admin tracks video <track_id|log_id> (--dir <dir> | --footage <file> [--footage-social <file>] [--footage-notext <file>] [--footage-landscape <file>] [--footage-landscape-social <file>] [--poster <file>] [--cover <file>] [--note <file>] [--composition <file>] [--props <file>] [--render <file>] [--intent <file>] [--metrics <file>] [--scene <file>]) [--allow-partial]");
7761
+ throw new Error("Missing id. Usage: fluncle admin tracks video <track_id|log_id> (--dir <dir> | --footage <file> [--footage-social <file>] [--footage-notext <file>] [--footage-landscape <file>] [--footage-landscape-social <file>] [--poster <file>] [--cover <file>] [--note <file>] [--composition <file>] [--props <file>] [--render <file>] [--intent <file>] [--metrics <file>] [--scene <file>] | --plate <file> [--plate-background <file>]) [--allow-partial]");
7701
7762
  }
7702
7763
  const dir = options.dir ? path2.resolve(process.cwd(), options.dir) : undefined;
7703
7764
  const fromDir = (name) => {
@@ -7725,14 +7786,17 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
7725
7786
  metrics: resolveFile(options.metrics, "metrics.json"),
7726
7787
  model: options.model,
7727
7788
  note: resolveFile(options.note, "note.txt"),
7789
+ plate: resolveFile(options.plate, "plate.png"),
7790
+ plateBackground: resolveFile(options.plateBackground, "plate.background.png"),
7728
7791
  poster: resolveFile(options.poster, "poster.jpg"),
7729
7792
  props: resolveFile(options.props, "props.json"),
7730
7793
  reasoning: options.reasoning,
7731
7794
  render: resolveFile(options.render, "render.json"),
7732
7795
  scene: resolveFile(options.scene, "scene.json")
7733
7796
  };
7734
- if (!files.footage && !options.allowPartial) {
7735
- throw new Error("A footage cut is required (--footage <file>, or --dir containing footage.mp4). Pass --allow-partial for a deliberate partial refresh (e.g. poster-only).");
7797
+ const { isPlatesOnlyUpload: isPlatesOnlyUpload2 } = await Promise.resolve().then(() => (init_track(), exports_track));
7798
+ if (!files.footage && !options.allowPartial && !isPlatesOnlyUpload2(files)) {
7799
+ throw new Error("A footage cut is required (--footage <file>, or --dir containing footage.mp4). Pass --allow-partial for a deliberate partial refresh (e.g. poster-only), or upload plates alone (--plate/--plate-background) for the plate-lane pre-upload.");
7736
7800
  }
7737
7801
  const onProgress = options.json ? undefined : (message) => console.log(message);
7738
7802
  const result = await trackVideoCommand2(idOrLogId, files, onProgress, {
@@ -8164,7 +8228,7 @@ function resolveHasKey(options) {
8164
8228
  return;
8165
8229
  }
8166
8230
  async function runAdminTracksList(options, listCommand2) {
8167
- const limit = parseListLimit(options.limit);
8231
+ const limit = options.all ? Number.POSITIVE_INFINITY : parseListLimit(options.limit);
8168
8232
  const order = options.order === "asc" ? "asc" : "desc";
8169
8233
  const hasKey = resolveHasKey(options);
8170
8234
  const tracks = await listCommand2({ hasKey, limit, order });
@@ -8531,6 +8595,8 @@ var stringOptions = new Set([
8531
8595
  "--metrics",
8532
8596
  "--mime",
8533
8597
  "--note",
8598
+ "--plate",
8599
+ "--plate-background",
8534
8600
  "--platform",
8535
8601
  "--poster",
8536
8602
  "--props",
package/package.json CHANGED
@@ -31,5 +31,5 @@
31
31
  "url": "git+https://github.com/mauricekleine/fluncle.git"
32
32
  },
33
33
  "type": "module",
34
- "version": "0.89.0"
34
+ "version": "0.91.0"
35
35
  }