fluncle 0.87.0 → 0.89.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 +89 -188
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -538,7 +538,7 @@ function parseVersion(version) {
538
538
  var currentVersion;
539
539
  var init_version = __esm(() => {
540
540
  init_output();
541
- currentVersion = "0.87.0".trim() ? "0.87.0".trim() : "0.1.0";
541
+ currentVersion = "0.89.0".trim() ? "0.89.0".trim() : "0.1.0";
542
542
  });
543
543
 
544
544
  // src/update-notifier.ts
@@ -1335,47 +1335,22 @@ __export(exports_mixtapes, {
1335
1335
  mixtapesCommand: () => mixtapesCommand,
1336
1336
  mixtapeUpdateCommand: () => mixtapeUpdateCommand,
1337
1337
  mixtapeResyncCommand: () => mixtapeResyncCommand,
1338
- mixtapePublishCommand: () => mixtapePublishCommand,
1339
- mixtapeMembersCommand: () => mixtapeMembersCommand,
1340
1338
  mixtapeListCommand: () => mixtapeListCommand,
1341
1339
  mixtapeGetCommand: () => mixtapeGetCommand,
1342
- mixtapeDistributeCommand: () => mixtapeDistributeCommand,
1343
- mixtapeDeleteCommand: () => mixtapeDeleteCommand,
1344
- mixtapeCreateCommand: () => mixtapeCreateCommand
1340
+ mixtapeDistributeCommand: () => mixtapeDistributeCommand
1345
1341
  });
1346
- import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1347
1342
  async function mixtapesCommand() {
1348
1343
  const response = await publicApiGet("/api/mixtapes");
1349
1344
  return response.mixtapes;
1350
1345
  }
1351
- async function mixtapeCreateCommand(options) {
1352
- return adminApiPost("/api/admin/mixtapes", buildBody(options));
1353
- }
1354
1346
  async function mixtapeUpdateCommand(id, options) {
1355
1347
  return adminApiPatch(`/api/admin/mixtapes/${encodeURIComponent(id)}`, buildBody(options));
1356
1348
  }
1357
- async function mixtapeMembersCommand(id, refs, options) {
1358
- const members = refs.map((ref) => ({ ref }));
1359
- if (options.from) {
1360
- members.push(...parseCueFile(options.from));
1361
- }
1362
- return adminApiPut(`/api/admin/mixtapes/${encodeURIComponent(id)}/members`, { members });
1363
- }
1364
- async function mixtapePublishCommand(id) {
1365
- return adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(id)}/publish`);
1366
- }
1367
- async function mixtapeDeleteCommand(id) {
1368
- return adminApiDelete(`/api/admin/mixtapes/${encodeURIComponent(id)}`);
1369
- }
1370
1349
  async function mixtapeDistributeCommand(idOrLogId, options, onProgress = () => {}) {
1371
1350
  const mixtape = await mixtapeGetCommand(idOrLogId);
1372
1351
  if (!mixtape.id) {
1373
1352
  throw new CliError2("mixtape_not_found", `No mixtape with id or log id ${idOrLogId}`);
1374
1353
  }
1375
- if (mixtape.status === "draft") {
1376
- throw new CliError2("mixtape_not_promoted", `${mixtape.id} is still a draft — promote its recording first:
1377
- ` + " fluncle admin recordings promote <recordingId>");
1378
- }
1379
1354
  const both = !options.youtube && !options.mixcloud;
1380
1355
  const doYoutube = both || Boolean(options.youtube);
1381
1356
  const doMixcloud = both || Boolean(options.mixcloud);
@@ -1388,7 +1363,8 @@ async function mixtapeDistributeCommand(idOrLogId, options, onProgress = () => {
1388
1363
  const mixtapeId = mixtape.id;
1389
1364
  const logId = mixtape.logId;
1390
1365
  if (!logId) {
1391
- throw new CliError2("missing_log_id", "The mixtape has no Log IDwas it promoted successfully?");
1366
+ throw new CliError2("mixtape_not_promoted", `${mixtapeId} has no coordinate yetpromote its recording first:
1367
+ ` + " fluncle admin recordings promote <recordingId>");
1392
1368
  }
1393
1369
  if (!mixtape.durationMs) {
1394
1370
  const source = options.audio ?? options.video;
@@ -1495,65 +1471,6 @@ function buildBody(options) {
1495
1471
  }
1496
1472
  return body;
1497
1473
  }
1498
- function parseCueFile(filePath) {
1499
- if (!existsSync(filePath)) {
1500
- throw new CliError2("file_not_found", `Cue file not found: ${filePath}`);
1501
- }
1502
- const text = readFileSync2(filePath, "utf-8");
1503
- const trimmed = text.trim();
1504
- if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
1505
- try {
1506
- const parsed = JSON.parse(trimmed);
1507
- if (!Array.isArray(parsed)) {
1508
- throw new Error("not an array");
1509
- }
1510
- return parsed.map((entry, index) => {
1511
- if (typeof entry === "string") {
1512
- return { ref: entry.trim() };
1513
- }
1514
- const obj = entry;
1515
- if (typeof obj?.ref !== "string") {
1516
- throw new CliError2("invalid_cue_json", `Entry ${index + 1} missing "ref" string`);
1517
- }
1518
- const cue = { ref: obj.ref.trim() };
1519
- if (typeof obj.startMs === "number" && Number.isInteger(obj.startMs) && obj.startMs >= 0) {
1520
- cue.startMs = obj.startMs;
1521
- }
1522
- return cue;
1523
- });
1524
- } catch (error) {
1525
- if (error instanceof CliError2) {
1526
- throw error;
1527
- }
1528
- throw new CliError2("invalid_cue_json", `Cue JSON parse failed: ${error instanceof Error ? error.message : String(error)}`);
1529
- }
1530
- }
1531
- return parseCueSheet(text);
1532
- }
1533
- function parseCueSheet(text) {
1534
- const entries = [];
1535
- for (const line of text.split(/\r?\n/)) {
1536
- const trimmed = line.trim();
1537
- if (!trimmed || trimmed.startsWith("#")) {
1538
- continue;
1539
- }
1540
- const match = trimmed.match(/^(\d{1,2}:\d{2}(?::\d{2})?)\s+(.+)$/);
1541
- if (match) {
1542
- const [, time, ref] = match;
1543
- if (time === undefined || ref === undefined) {
1544
- continue;
1545
- }
1546
- const startMs = parseDuration(time);
1547
- if (startMs === null) {
1548
- continue;
1549
- }
1550
- entries.push({ ref: ref.trim(), startMs });
1551
- } else {
1552
- entries.push({ ref: trimmed });
1553
- }
1554
- }
1555
- return entries;
1556
- }
1557
1474
  var init_mixtapes = __esm(() => {
1558
1475
  init_util();
1559
1476
  init_api();
@@ -2251,7 +2168,7 @@ function parseVersion2(version) {
2251
2168
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2252
2169
  var init_version2 = __esm(() => {
2253
2170
  init_output();
2254
- currentVersion2 = "0.87.0".trim() ? "0.87.0".trim() : "0.1.0";
2171
+ currentVersion2 = "0.89.0".trim() ? "0.89.0".trim() : "0.1.0";
2255
2172
  });
2256
2173
 
2257
2174
  // ../../packages/registry/src/index.ts
@@ -2773,7 +2690,7 @@ var init_src = __esm(() => {
2773
2690
  {
2774
2691
  command: "fluncle admin",
2775
2692
  exposedContent: [
2776
- "the operator/agent command group (hidden): tracks publish|update|enrich|video|draft|social|preview|observe|context|note, mixtapes create|update|members|publish|delete, newsletter draft|update|send|list, backfills, auth"
2693
+ "the operator/agent command group (hidden): tracks publish|update|enrich|video|draft|social|preview|observe|context|note, recordings create|promote, mixtapes update|distribute|resync, newsletter draft|update|send|list, backfills, auth"
2777
2694
  ],
2778
2695
  kind: "cli",
2779
2696
  name: "cli.admin",
@@ -2977,16 +2894,41 @@ __export(exports_track, {
2977
2894
  trackNoteCommand: () => trackNoteCommand,
2978
2895
  trackGetCommand: () => trackGetCommand,
2979
2896
  trackDraftCommand: () => trackDraftCommand,
2980
- trackContextCommand: () => trackContextCommand
2897
+ trackContextCommand: () => trackContextCommand,
2898
+ checkBundleCompleteness: () => checkBundleCompleteness
2981
2899
  });
2982
2900
  async function trackGetCommand(idOrLogId) {
2983
2901
  return publicApiGet(`/api/tracks/${encodeURIComponent(idOrLogId)}`);
2984
2902
  }
2985
- async function trackVideoCommand(idOrLogId, files, onProgress) {
2903
+ function checkBundleCompleteness(files) {
2904
+ const uploadingFootage = FOOTAGE_FIELDS.some((option) => Boolean(files[option]));
2905
+ const missingFrom = (specs) => uploadingFootage ? specs.filter((spec) => !files[spec.option]).map((spec) => spec.file) : [];
2906
+ return {
2907
+ missingAdvisory: missingFrom(RERENDER_ADVISORY_FIELDS),
2908
+ missingContract: missingFrom(RERENDER_CONTRACT_FIELDS),
2909
+ uploadingFootage
2910
+ };
2911
+ }
2912
+ async function trackVideoCommand(idOrLogId, files, onProgress, options = {}) {
2913
+ const completeness = checkBundleCompleteness(files);
2914
+ if (completeness.uploadingFootage && completeness.missingContract.length > 0 && !options.allowPartial) {
2915
+ throw new CliError2("bundle_incomplete", `Refusing to upload a PARTIAL bundle: footage is being uploaded but the re-render contract is missing ${completeness.missingContract.join(", ")}. ` + `A footage-only upload leaves composition.tsx/props.json/render.json stale on R2 and desyncs the render.json from the DB ledger. ` + `Ship the complete bundle (re-run \`ship\` and upload with --dir), or pass --allow-partial for a deliberate partial refresh (e.g. poster-only).`);
2916
+ }
2917
+ if (completeness.uploadingFootage) {
2918
+ if (completeness.missingContract.length > 0) {
2919
+ onProgress?.(`warning: --allow-partial — uploading WITHOUT the re-render contract (${completeness.missingContract.join(", ")}); the R2 bundle will NOT be re-renderable`);
2920
+ }
2921
+ for (const missing of completeness.missingAdvisory) {
2922
+ onProgress?.(`warning: ${missing} missing (provenance/eval only) — shipping without it`);
2923
+ }
2924
+ }
2986
2925
  const present = VIDEO_FIELDS.map((spec) => ({
2987
2926
  field: spec.field,
2988
2927
  path: files[spec.option]
2989
2928
  })).filter((spec) => Boolean(spec.path));
2929
+ if (present.length === 0) {
2930
+ throw new CliError2("nothing_to_upload", "No bundle files resolved to upload (pass --dir <bundle> or explicit file flags).");
2931
+ }
2990
2932
  const presign = await adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/video/uploads`, { fields: present.map((spec) => spec.field) });
2991
2933
  const byField = new Map(presign.uploads.map((upload) => [upload.field, upload]));
2992
2934
  const urls = {};
@@ -3116,7 +3058,7 @@ async function trackNoteCommand(idOrLogId, options) {
3116
3058
  const body = { note: options.note };
3117
3059
  return adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/note`, body);
3118
3060
  }
3119
- var DEFAULT_VIDEO_MODEL = "anthropic/claude-opus-4-8", DEFAULT_VIDEO_REASONING = "high", FOUND_BASE = "https://found.fluncle.com", VIDEO_FIELDS;
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;
3120
3062
  var init_track = __esm(() => {
3121
3063
  init_api();
3122
3064
  init_output();
@@ -3133,7 +3075,25 @@ var init_track = __esm(() => {
3133
3075
  { field: "props", option: "props" },
3134
3076
  { field: "render", option: "render" },
3135
3077
  { field: "intent", option: "intent" },
3136
- { field: "metrics", option: "metrics" }
3078
+ { field: "metrics", option: "metrics" },
3079
+ { field: "scene", option: "scene" }
3080
+ ];
3081
+ RERENDER_CONTRACT_FIELDS = [
3082
+ { file: "composition.tsx", option: "composition" },
3083
+ { file: "props.json", option: "props" },
3084
+ { file: "render.json", option: "render" }
3085
+ ];
3086
+ RERENDER_ADVISORY_FIELDS = [
3087
+ { file: "intent.json", option: "intent" },
3088
+ { file: "metrics.json", option: "metrics" },
3089
+ { file: "scene.json", option: "scene" }
3090
+ ];
3091
+ FOOTAGE_FIELDS = [
3092
+ "footage",
3093
+ "footageSocial",
3094
+ "footageNotext",
3095
+ "footageLandscape",
3096
+ "footageLandscapeSocial"
3137
3097
  ];
3138
3098
  });
3139
3099
 
@@ -3668,7 +3628,7 @@ var init_mixtape_youtube2 = __esm(() => {
3668
3628
 
3669
3629
  // src/commands/mixtape-set-video.ts
3670
3630
  import { randomUUID } from "node:crypto";
3671
- import { existsSync as existsSync2, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
3631
+ import { existsSync, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
3672
3632
  import { tmpdir } from "node:os";
3673
3633
  import { join as join4 } from "node:path";
3674
3634
  function planMultipart(contentLength, partSize = DEFAULT_PART_SIZE) {
@@ -3725,7 +3685,7 @@ function renditionFfmpegArgs(inputPath, outputPath) {
3725
3685
  ];
3726
3686
  }
3727
3687
  async function uploadRenditionMultipart(masterPath, presignPath, onProgress = () => {}) {
3728
- if (!existsSync2(masterPath)) {
3688
+ if (!existsSync(masterPath)) {
3729
3689
  throw new CliError2("file_not_found", `Set-video master not found: ${masterPath}`);
3730
3690
  }
3731
3691
  await assertFfmpeg();
@@ -3838,7 +3798,7 @@ __export(exports_recordings, {
3838
3798
  recordingDeleteCommand: () => recordingDeleteCommand,
3839
3799
  recordingCreateCommand: () => recordingCreateCommand
3840
3800
  });
3841
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
3801
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
3842
3802
  async function recordingGet(id) {
3843
3803
  const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
3844
3804
  return response.recording;
@@ -3907,7 +3867,7 @@ async function recordingCreateCommand(options = {}) {
3907
3867
  if (!options.video) {
3908
3868
  throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
3909
3869
  }
3910
- if (!existsSync3(options.video)) {
3870
+ if (!existsSync2(options.video)) {
3911
3871
  throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
3912
3872
  }
3913
3873
  const created = await adminApiPost("/api/admin/recordings", {
@@ -3943,11 +3903,11 @@ async function recordingUpdateCommand(id, options = {}) {
3943
3903
  body.parentId = options.parentId;
3944
3904
  }
3945
3905
  if (options.tracklistFile !== undefined) {
3946
- if (!existsSync3(options.tracklistFile)) {
3906
+ if (!existsSync2(options.tracklistFile)) {
3947
3907
  throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
3948
3908
  }
3949
3909
  try {
3950
- body.tracklistJson = JSON.parse(readFileSync3(options.tracklistFile, "utf8"));
3910
+ body.tracklistJson = JSON.parse(readFileSync2(options.tracklistFile, "utf8"));
3951
3911
  } catch {
3952
3912
  throw new CliError2("invalid_tracklist", `Tracklist file is not valid JSON: ${options.tracklistFile}`);
3953
3913
  }
@@ -3992,12 +3952,12 @@ async function recordingReplaceCuesCommand(id, options = {}) {
3992
3952
  if (!options.cuesFile) {
3993
3953
  throw new CliError2("missing_cues_file", "replace-cues needs a --cues-file <file> (the ordered cue array)");
3994
3954
  }
3995
- if (!existsSync3(options.cuesFile)) {
3955
+ if (!existsSync2(options.cuesFile)) {
3996
3956
  throw new CliError2("file_not_found", `Cues file not found: ${options.cuesFile}`);
3997
3957
  }
3998
3958
  let cues;
3999
3959
  try {
4000
- cues = JSON.parse(readFileSync3(options.cuesFile, "utf8"));
3960
+ cues = JSON.parse(readFileSync2(options.cuesFile, "utf8"));
4001
3961
  } catch {
4002
3962
  throw new CliError2("invalid_cues", `Cues file is not valid JSON: ${options.cuesFile}`);
4003
3963
  }
@@ -4201,7 +4161,7 @@ __export(exports_recordings2, {
4201
4161
  recordingDeleteCommand: () => recordingDeleteCommand2,
4202
4162
  recordingCreateCommand: () => recordingCreateCommand2
4203
4163
  });
4204
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
4164
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
4205
4165
  async function recordingGet2(id) {
4206
4166
  const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
4207
4167
  return response.recording;
@@ -4270,7 +4230,7 @@ async function recordingCreateCommand2(options = {}) {
4270
4230
  if (!options.video) {
4271
4231
  throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
4272
4232
  }
4273
- if (!existsSync4(options.video)) {
4233
+ if (!existsSync3(options.video)) {
4274
4234
  throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
4275
4235
  }
4276
4236
  const created = await adminApiPost("/api/admin/recordings", {
@@ -4306,11 +4266,11 @@ async function recordingUpdateCommand2(id, options = {}) {
4306
4266
  body.parentId = options.parentId;
4307
4267
  }
4308
4268
  if (options.tracklistFile !== undefined) {
4309
- if (!existsSync4(options.tracklistFile)) {
4269
+ if (!existsSync3(options.tracklistFile)) {
4310
4270
  throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
4311
4271
  }
4312
4272
  try {
4313
- body.tracklistJson = JSON.parse(readFileSync4(options.tracklistFile, "utf8"));
4273
+ body.tracklistJson = JSON.parse(readFileSync3(options.tracklistFile, "utf8"));
4314
4274
  } catch {
4315
4275
  throw new CliError2("invalid_tracklist", `Tracklist file is not valid JSON: ${options.tracklistFile}`);
4316
4276
  }
@@ -4355,12 +4315,12 @@ async function recordingReplaceCuesCommand2(id, options = {}) {
4355
4315
  if (!options.cuesFile) {
4356
4316
  throw new CliError2("missing_cues_file", "replace-cues needs a --cues-file <file> (the ordered cue array)");
4357
4317
  }
4358
- if (!existsSync4(options.cuesFile)) {
4318
+ if (!existsSync3(options.cuesFile)) {
4359
4319
  throw new CliError2("file_not_found", `Cues file not found: ${options.cuesFile}`);
4360
4320
  }
4361
4321
  let cues;
4362
4322
  try {
4363
- cues = JSON.parse(readFileSync4(options.cuesFile, "utf8"));
4323
+ cues = JSON.parse(readFileSync3(options.cuesFile, "utf8"));
4364
4324
  } catch {
4365
4325
  throw new CliError2("invalid_cues", `Cues file is not valid JSON: ${options.cuesFile}`);
4366
4326
  }
@@ -4386,7 +4346,7 @@ __export(exports_newsletter, {
4386
4346
  newsletterDraftCommand: () => newsletterDraftCommand,
4387
4347
  newsletterDeleteCommand: () => newsletterDeleteCommand
4388
4348
  });
4389
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
4349
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
4390
4350
  function buildBody2(options, { requireContent }) {
4391
4351
  const body = {};
4392
4352
  if (options.contentFile !== undefined) {
@@ -4406,10 +4366,10 @@ function buildBody2(options, { requireContent }) {
4406
4366
  return body;
4407
4367
  }
4408
4368
  function readContentFile(filePath) {
4409
- if (!existsSync5(filePath)) {
4369
+ if (!existsSync4(filePath)) {
4410
4370
  throw new CliError2("file_not_found", `Content file not found: ${filePath}`);
4411
4371
  }
4412
- const text = readFileSync5(filePath, "utf-8");
4372
+ const text = readFileSync4(filePath, "utf-8");
4413
4373
  try {
4414
4374
  return JSON.parse(text);
4415
4375
  } catch (error) {
@@ -5044,7 +5004,7 @@ var init_format2 = __esm(() => {
5044
5004
  });
5045
5005
 
5046
5006
  // src/cli.ts
5047
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
5007
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
5048
5008
  import path2 from "path";
5049
5009
 
5050
5010
  // ../../node_modules/commander/lib/error.js
@@ -7312,7 +7272,7 @@ function addAdminCommands(program2) {
7312
7272
  const { trackUpdateCommand: trackUpdateCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
7313
7273
  await runTrackUpdate(trackId, options, trackUpdateCommand2);
7314
7274
  });
7315
- adminTrack.command("video").description("Upload a track's video bundle to R2 and link it").argument("[idOrLogId]").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").allowExcessArguments().action(async (idOrLogId, options) => {
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) => {
7316
7276
  const { trackVideoCommand: trackVideoCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
7317
7277
  await runTrackVideo(idOrLogId, options, trackVideoCommand2);
7318
7278
  });
@@ -7372,27 +7332,11 @@ function addAdminCommands(program2) {
7372
7332
  adminMixtapes.action(() => {
7373
7333
  adminMixtapes.outputHelp();
7374
7334
  });
7375
- adminMixtapes.command("create").description("Log a new mixtape draft").option("--duration-ms <duration>", "Duration (mm:ss, h:mm:ss, or ms)").option("--json", "Print JSON", false).option("--note <text>", "Operator note").option("--recorded-at <date>", "Recorded date (ISO)").option("--soundcloud-url <url>", "SoundCloud URL (manual; YouTube + Mixcloud come from distribute)").allowExcessArguments().action(async (options) => {
7376
- const { mixtapeCreateCommand: mixtapeCreateCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
7377
- await runMixtapeCreate(options, mixtapeCreateCommand2);
7378
- });
7379
7335
  adminMixtapes.command("update").description("Update a mixtape's fields").argument("[id]").option("--duration-ms <duration>", "Duration (mm:ss, h:mm:ss, or ms)").option("--json", "Print JSON", false).option("--note <text>", "Operator note").option("--recorded-at <date>", "Recorded date (ISO)").option("--soundcloud-url <url>", "SoundCloud URL (manual; YouTube + Mixcloud come from distribute)").allowExcessArguments().action(async (id, options) => {
7380
7336
  const { mixtapeUpdateCommand: mixtapeUpdateCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
7381
7337
  await runMixtapeUpdate(id, options, mixtapeUpdateCommand2);
7382
7338
  });
7383
- adminMixtapes.command("members").description("Set a mixtape's tracklist (refs and/or a cue-sheet file)").argument("[id]").argument("[refs...]").option("--from <file>", "Cue-sheet or JSON file with members").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, refs, options) => {
7384
- const { mixtapeMembersCommand: mixtapeMembersCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
7385
- await runMixtapeMembers(id, refs, options, mixtapeMembersCommand2);
7386
- });
7387
- adminMixtapes.command("publish").description("Publish a mixtape draft").argument("[id]").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
7388
- const { mixtapePublishCommand: mixtapePublishCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
7389
- await runMixtapePublish(id, options, mixtapePublishCommand2);
7390
- });
7391
- adminMixtapes.command("delete").description("Discard a mixtape draft (published mixtapes can't be deleted)").argument("[id]").option("--json", "Print JSON", false).option("--yes", "Skip confirmation", false).allowExcessArguments().action(async (id, options) => {
7392
- const { mixtapeDeleteCommand: mixtapeDeleteCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
7393
- await runMixtapeDelete(id, options, mixtapeDeleteCommand2);
7394
- });
7395
- adminMixtapes.command("list").description("List all mixtapes (including drafts)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
7339
+ adminMixtapes.command("list").description("List all mixtapes (including distributing)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
7396
7340
  const { mixtapeListCommand: mixtapeListCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
7397
7341
  await runMixtapeList(options, mixtapeListCommand2);
7398
7342
  });
@@ -7559,7 +7503,7 @@ async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCo
7559
7503
  console.log(` mime: ${result.mime}`);
7560
7504
  }
7561
7505
  async function runTrackObserve(idOrLogId, options, trackObserveCommand2) {
7562
- const script = options.scriptFile ? readFileSync6(options.scriptFile, "utf8") : options.script;
7506
+ const script = options.scriptFile ? readFileSync5(options.scriptFile, "utf8") : options.script;
7563
7507
  if (!idOrLogId || !script || !script.trim()) {
7564
7508
  throw new Error("Usage: fluncle admin tracks observe <track_id|log_id> (--script <text> | --script-file <file>) [--voice-id <id>] [--duration-ms <ms>] [--context-note <text>] [--json]");
7565
7509
  }
@@ -7615,7 +7559,7 @@ async function runTrackContext(idOrLogId, options, trackContextCommand2) {
7615
7559
  }
7616
7560
  }
7617
7561
  async function runTrackNote(idOrLogId, options, trackNoteCommand2) {
7618
- const note = options.scriptFile ? readFileSync6(options.scriptFile, "utf8") : options.script;
7562
+ const note = options.scriptFile ? readFileSync5(options.scriptFile, "utf8") : options.script;
7619
7563
  if (!idOrLogId || !note || !note.trim()) {
7620
7564
  throw new Error("Usage: fluncle admin tracks note <track_id|log_id> (--script <text> | --script-file <file>) [--json]");
7621
7565
  }
@@ -7753,7 +7697,7 @@ async function runBackfillDiscogs(options, backfillDiscogsCommand2) {
7753
7697
  }
7754
7698
  async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
7755
7699
  if (!idOrLogId) {
7756
- 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>])");
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]");
7757
7701
  }
7758
7702
  const dir = options.dir ? path2.resolve(process.cwd(), options.dir) : undefined;
7759
7703
  const fromDir = (name) => {
@@ -7761,7 +7705,7 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
7761
7705
  return;
7762
7706
  }
7763
7707
  const candidate = path2.join(dir, name);
7764
- return existsSync6(candidate) ? candidate : undefined;
7708
+ return existsSync5(candidate) ? candidate : undefined;
7765
7709
  };
7766
7710
  const resolveFile = (explicit, name) => {
7767
7711
  if (explicit) {
@@ -7784,13 +7728,16 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
7784
7728
  poster: resolveFile(options.poster, "poster.jpg"),
7785
7729
  props: resolveFile(options.props, "props.json"),
7786
7730
  reasoning: options.reasoning,
7787
- render: resolveFile(options.render, "render.json")
7731
+ render: resolveFile(options.render, "render.json"),
7732
+ scene: resolveFile(options.scene, "scene.json")
7788
7733
  };
7789
- if (!files.footage) {
7790
- throw new Error("A footage cut is required (--footage <file>, or --dir containing footage.mp4)");
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).");
7791
7736
  }
7792
7737
  const onProgress = options.json ? undefined : (message) => console.log(message);
7793
- const result = await trackVideoCommand2(idOrLogId, files, onProgress);
7738
+ const result = await trackVideoCommand2(idOrLogId, files, onProgress, {
7739
+ allowPartial: options.allowPartial
7740
+ });
7794
7741
  if (options.json) {
7795
7742
  printJson(result);
7796
7743
  return;
@@ -7938,14 +7885,6 @@ async function runTrackPurgeVideo(idOrLogId, options, trackPurgeVideoCommand2) {
7938
7885
  }
7939
7886
  console.log(result.noVideo ? `${result.logId} has no video \u2014 nothing to purge.` : `Purging the stale renditions for ${result.logId} from the edge. The next play picks up the fresh render.`);
7940
7887
  }
7941
- async function runMixtapeCreate(options, mixtapeCreateCommand2) {
7942
- const result = await mixtapeCreateCommand2(options);
7943
- if (options.json) {
7944
- printJson(result);
7945
- return;
7946
- }
7947
- console.log(`Logged draft ${result.mixtape.id}. It stays a draft until you publish it.`);
7948
- }
7949
7888
  async function runMixtapeUpdate(id, options, mixtapeUpdateCommand2) {
7950
7889
  if (!id) {
7951
7890
  throw new Error("Missing mixtape id. Usage: fluncle admin mixtapes update <id>");
@@ -7957,31 +7896,6 @@ async function runMixtapeUpdate(id, options, mixtapeUpdateCommand2) {
7957
7896
  }
7958
7897
  console.log(`Saved ${result.mixtape.id}.`);
7959
7898
  }
7960
- async function runMixtapeMembers(id, refs, options, mixtapeMembersCommand2) {
7961
- if (!id) {
7962
- throw new Error("Missing mixtape id. Usage: fluncle admin mixtapes members <id> [refs...]");
7963
- }
7964
- if (refs.length === 0 && !options.from) {
7965
- throw new Error("Provide refs as arguments or a cue-sheet file with --from");
7966
- }
7967
- const result = await mixtapeMembersCommand2(id, refs, options);
7968
- if (options.json) {
7969
- printJson(result);
7970
- return;
7971
- }
7972
- console.log(`Saved the tracklist: ${result.mixtape.memberCount} bangers on ${result.mixtape.id}.`);
7973
- }
7974
- async function runMixtapePublish(id, options, mixtapePublishCommand2) {
7975
- if (!id) {
7976
- throw new Error("Missing mixtape id. Usage: fluncle admin mixtapes publish <id>");
7977
- }
7978
- const result = await mixtapePublishCommand2(id);
7979
- if (options.json) {
7980
- printJson(result);
7981
- return;
7982
- }
7983
- console.log(`Minted ${result.mixtape.logId} (fluncle://${result.mixtape.logId}) \u2014 distributing. ` + "Run `distribute` to push it to the platforms.");
7984
- }
7985
7899
  async function runMixtapeDistribute(idOrLogId, options, mixtapeDistributeCommand2) {
7986
7900
  if (!idOrLogId) {
7987
7901
  throw new Error("Missing mixtape id. Usage: fluncle admin mixtapes distribute <idOrLogId> --video <mp4> --audio <file>");
@@ -8023,20 +7937,6 @@ async function runMixtapeResync(idOrLogId, options, mixtapeResyncCommand2) {
8023
7937
  console.log(`Re-synced ${result.logId} (fluncle://${result.logId}).
8024
7938
  ${links}`);
8025
7939
  }
8026
- async function runMixtapeDelete(id, options, mixtapeDeleteCommand2) {
8027
- if (!id) {
8028
- throw new Error("Missing mixtape id. Usage: fluncle admin mixtapes delete <id> --yes");
8029
- }
8030
- if (!options.yes) {
8031
- throw new Error("Pass --yes to confirm the discard. Published mixtapes can't be deleted.");
8032
- }
8033
- await mixtapeDeleteCommand2(id);
8034
- if (options.json) {
8035
- printJson({ id, ok: true });
8036
- return;
8037
- }
8038
- console.log(`Discarded draft ${id}.`);
8039
- }
8040
7940
  async function runMixtapeList(options, mixtapeListCommand2) {
8041
7941
  const mixtapes = await mixtapeListCommand2();
8042
7942
  if (options.json) {
@@ -8048,8 +7948,8 @@ async function runMixtapeList(options, mixtapeListCommand2) {
8048
7948
  return;
8049
7949
  }
8050
7950
  for (const mixtape of mixtapes) {
8051
- const status = mixtape.status ?? "draft";
8052
- const coordinate3 = mixtape.logId ?? "draft";
7951
+ const status = mixtape.status ?? "distributing";
7952
+ const coordinate3 = mixtape.logId ?? "unminted";
8053
7953
  console.log(`${coordinate3} ${status} ${mixtape.memberCount} bangers ${mixtape.title}`);
8054
7954
  }
8055
7955
  }
@@ -8092,8 +7992,8 @@ async function runMixtapeGet(idOrLogId, options, mixtapeGetCommand2) {
8092
7992
  printJson(mixtape);
8093
7993
  return;
8094
7994
  }
8095
- const coordinate3 = mixtape.logId ?? "draft";
8096
- const status = mixtape.status ?? "draft";
7995
+ const coordinate3 = mixtape.logId ?? "unminted";
7996
+ const status = mixtape.status ?? "distributing";
8097
7997
  console.log(`${mixtape.title}`);
8098
7998
  console.log(` ${coordinate3} \xB7 ${status} \xB7 ${mixtape.memberCount} bangers`);
8099
7999
  if (mixtape.note) {
@@ -8637,6 +8537,7 @@ var stringOptions = new Set([
8637
8537
  "--query",
8638
8538
  "--recorded-at",
8639
8539
  "--render",
8540
+ "--scene",
8640
8541
  "--scheduled-for",
8641
8542
  "--soundcloud-url",
8642
8543
  "--source",
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.87.0"
34
+ "version": "0.89.0"
35
35
  }