fluncle 0.107.0 → 0.110.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/bin/fluncle.mjs +226 -336
- package/package.json +1 -1
package/bin/fluncle.mjs
CHANGED
|
@@ -557,7 +557,7 @@ function parseVersion(version) {
|
|
|
557
557
|
var currentVersion;
|
|
558
558
|
var init_version = __esm(() => {
|
|
559
559
|
init_output();
|
|
560
|
-
currentVersion = "0.
|
|
560
|
+
currentVersion = "0.110.0".trim() ? "0.110.0".trim() : "0.1.0";
|
|
561
561
|
});
|
|
562
562
|
|
|
563
563
|
// src/update-notifier.ts
|
|
@@ -1100,12 +1100,12 @@ async function distributeYoutube(mixtapeId, videoPath, onProgress) {
|
|
|
1100
1100
|
continue;
|
|
1101
1101
|
}
|
|
1102
1102
|
if (result.kind === "reauth") {
|
|
1103
|
-
onProgress?.("Access token expired
|
|
1103
|
+
onProgress?.("Access token expired, re-minting and resuming");
|
|
1104
1104
|
session = { accessToken: await mintToken(), sessionUri: session.sessionUri };
|
|
1105
1105
|
offset = await queryOffset(session.sessionUri, session.accessToken, contentLength);
|
|
1106
1106
|
continue;
|
|
1107
1107
|
}
|
|
1108
|
-
onProgress?.("Upload session expired
|
|
1108
|
+
onProgress?.("Upload session expired, re-initiating");
|
|
1109
1109
|
session = await initiate(mixtapeId, contentLength, contentType);
|
|
1110
1110
|
offset = 0;
|
|
1111
1111
|
}
|
|
@@ -1401,7 +1401,7 @@ async function mixtapeDistributeCommand(idOrLogId, options, onProgress = () => {
|
|
|
1401
1401
|
const mixtapeId = mixtape.id;
|
|
1402
1402
|
const logId = mixtape.logId;
|
|
1403
1403
|
if (!logId) {
|
|
1404
|
-
throw new CliError2("mixtape_not_promoted", `${mixtapeId} has no coordinate yet
|
|
1404
|
+
throw new CliError2("mixtape_not_promoted", `${mixtapeId} has no coordinate yet. Promote its recording first:
|
|
1405
1405
|
` + " fluncle admin recordings promote <recordingId>");
|
|
1406
1406
|
}
|
|
1407
1407
|
if (!mixtape.durationMs) {
|
|
@@ -1415,7 +1415,7 @@ async function mixtapeDistributeCommand(idOrLogId, options, onProgress = () => {
|
|
|
1415
1415
|
if (mixtape.status === "published") {
|
|
1416
1416
|
onProgress(`Already published (${logId}); re-distributing.`);
|
|
1417
1417
|
} else {
|
|
1418
|
-
onProgress(`Distributing ${logId} (promoted
|
|
1418
|
+
onProgress(`Distributing ${logId} (promoted, coordinate already minted).`);
|
|
1419
1419
|
}
|
|
1420
1420
|
const results = [];
|
|
1421
1421
|
if (doYoutube) {
|
|
@@ -1446,7 +1446,7 @@ async function mixtapeResyncCommand(idOrLogId, options, onProgress = () => {}) {
|
|
|
1446
1446
|
throw new CliError2("mixtape_not_found", `No mixtape with id or log id ${idOrLogId}`);
|
|
1447
1447
|
}
|
|
1448
1448
|
if (!mixtape.logId) {
|
|
1449
|
-
throw new CliError2("mixtape_no_log_id", "The mixtape isn't published yet
|
|
1449
|
+
throw new CliError2("mixtape_no_log_id", "The mixtape isn't published yet. Distribute it before re-syncing.");
|
|
1450
1450
|
}
|
|
1451
1451
|
const mixtapeId = mixtape.id;
|
|
1452
1452
|
const explicit = Boolean(options.youtube) || Boolean(options.mixcloud);
|
|
@@ -1917,7 +1917,7 @@ async function loginCommand() {
|
|
|
1917
1917
|
try {
|
|
1918
1918
|
await openExternal(verificationUrl);
|
|
1919
1919
|
} catch {
|
|
1920
|
-
console.log("Couldn't open a browser automatically
|
|
1920
|
+
console.log("Couldn't open a browser automatically. Visit the URL above to approve.");
|
|
1921
1921
|
}
|
|
1922
1922
|
const token = await pollForToken(baseUrl, device);
|
|
1923
1923
|
const user = await fetchSessionUser(baseUrl, token.access_token);
|
|
@@ -1939,7 +1939,7 @@ async function logoutCommand() {
|
|
|
1939
1939
|
});
|
|
1940
1940
|
}
|
|
1941
1941
|
const cleared = clearUserToken();
|
|
1942
|
-
console.log(cleared ? "Signed out. This device is no longer linked to your account." : "Not signed in
|
|
1942
|
+
console.log(cleared ? "Signed out. This device is no longer linked to your account." : "Not signed in. Nothing to do.");
|
|
1943
1943
|
}
|
|
1944
1944
|
async function requestDeviceCode(baseUrl) {
|
|
1945
1945
|
const response = await fetch(`${baseUrl}/api/auth/device/code`, {
|
|
@@ -2207,7 +2207,7 @@ function parseVersion2(version) {
|
|
|
2207
2207
|
var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
|
|
2208
2208
|
var init_version2 = __esm(() => {
|
|
2209
2209
|
init_output();
|
|
2210
|
-
currentVersion2 = "0.
|
|
2210
|
+
currentVersion2 = "0.110.0".trim() ? "0.110.0".trim() : "0.1.0";
|
|
2211
2211
|
});
|
|
2212
2212
|
|
|
2213
2213
|
// ../../packages/registry/src/index.ts
|
|
@@ -2679,7 +2679,7 @@ var init_src = __esm(() => {
|
|
|
2679
2679
|
{
|
|
2680
2680
|
command: "ssh rave.fluncle.com",
|
|
2681
2681
|
exposedContent: [
|
|
2682
|
-
"the rave terminal TUI:
|
|
2682
|
+
"the rave terminal TUI: Latest bangers, Mixtape archive, Submit, Subscribe, Install CLI, About",
|
|
2683
2683
|
"deep-register one-shots: `ssh rave.fluncle.com latest|random`"
|
|
2684
2684
|
],
|
|
2685
2685
|
kind: "ssh",
|
|
@@ -2797,7 +2797,7 @@ var init_src = __esm(() => {
|
|
|
2797
2797
|
],
|
|
2798
2798
|
kind: "cron",
|
|
2799
2799
|
name: "cron.embed",
|
|
2800
|
-
operatorNotes: "every 5m. On-box MuQ (torch, ~16s/track), zero LLM tokens. Writes the vector via the agent-tier update_track. Source: docs/agents/hermes/scripts/embed-sweep.* + embed-track.py. See docs/
|
|
2800
|
+
operatorNotes: "every 5m. On-box MuQ (torch, ~16s/track), zero LLM tokens. Writes the vector via the agent-tier update_track. Source: docs/agents/hermes/scripts/embed-sweep.* + embed-track.py. See docs/track-lifecycle.md.",
|
|
2801
2801
|
probeConfig: { cadenceMs: 5 * MINUTE_MS, cronName: "fluncle-embed", kind: "cron" },
|
|
2802
2802
|
weights: { status: "hidden" }
|
|
2803
2803
|
},
|
|
@@ -2808,7 +2808,7 @@ var init_src = __esm(() => {
|
|
|
2808
2808
|
],
|
|
2809
2809
|
kind: "cron",
|
|
2810
2810
|
name: "cron.capture",
|
|
2811
|
-
operatorNotes: "every 5m, run by a rave-02 HOST systemd timer (docs/agents/hermes/capture-timer/) — NOT a gateway cron: a proxied yt-dlp fetch has an unbounded tail that would starve the 5-min enrich/context/note sweeps on the shared serial runner. A NON-BLOCKING side-channel (never gates enrich/embed). Runs yt-dlp through a residential proxy on a per-track sticky session, duration-guards the match, stores the full song in the PRIVATE fluncle-source-audio bucket (S3-direct), and writes back via the agent-tier update_track (with per-finding backoff). yt-dlp + ffprobe are a box deploy prereq. Newest-first so a fresh add jumps the backfill. Source: docs/agents/hermes/scripts/capture-sweep.*. See docs/
|
|
2811
|
+
operatorNotes: "every 5m, run by a rave-02 HOST systemd timer (docs/agents/hermes/capture-timer/) — NOT a gateway cron: a proxied yt-dlp fetch has an unbounded tail that would starve the 5-min enrich/context/note sweeps on the shared serial runner. A NON-BLOCKING side-channel (never gates enrich/embed). Runs yt-dlp through a residential proxy on a per-track sticky session, duration-guards the match, stores the full song in the PRIVATE fluncle-source-audio bucket (S3-direct), and writes back via the agent-tier update_track (with per-finding backoff). yt-dlp + ffprobe are a box deploy prereq. Newest-first so a fresh add jumps the backfill. Source: docs/agents/hermes/scripts/capture-sweep.*. See docs/track-lifecycle.md.",
|
|
2812
2812
|
probeConfig: { cadenceMs: 5 * MINUTE_MS, cronName: "fluncle-capture", kind: "cron" },
|
|
2813
2813
|
weights: { status: "hidden" }
|
|
2814
2814
|
},
|
|
@@ -2928,7 +2928,8 @@ var init_src = __esm(() => {
|
|
|
2928
2928
|
probeConfig: {
|
|
2929
2929
|
cadenceMs: 7 * 24 * 60 * MINUTE_MS,
|
|
2930
2930
|
cronName: "fluncle-newsletter",
|
|
2931
|
-
kind: "cron"
|
|
2931
|
+
kind: "cron",
|
|
2932
|
+
schedule: { time: "15:00", tz: "Europe/Amsterdam", weekday: 5 }
|
|
2932
2933
|
},
|
|
2933
2934
|
weights: { status: "secondary" }
|
|
2934
2935
|
},
|
|
@@ -2939,7 +2940,42 @@ var init_src = __esm(() => {
|
|
|
2939
2940
|
kind: "cron",
|
|
2940
2941
|
name: "cron.backup",
|
|
2941
2942
|
operatorNotes: "daily. An OWNED, off-Cloudflare backup: dumps prod Turso over the libSQL HTTP pipeline → gzip → a PRIVATE R2 bucket (never fluncle-videos, which is world-served at found.fluncle.com) + prune. Zero LLM tokens; talks to Turso + R2 directly (no fluncle CLI, no agent token). Turso's managed PITR is the belt; this is the braces. Restore is proven by apps/web/scripts/restore-drill.ts. Source: docs/agents/hermes/scripts/backup-sweep.*",
|
|
2942
|
-
probeConfig: {
|
|
2943
|
+
probeConfig: {
|
|
2944
|
+
cadenceMs: 24 * 60 * MINUTE_MS,
|
|
2945
|
+
cronName: "fluncle-backup",
|
|
2946
|
+
kind: "cron",
|
|
2947
|
+
schedule: { time: "03:00", tz: "Europe/Amsterdam" }
|
|
2948
|
+
},
|
|
2949
|
+
weights: { status: "secondary" }
|
|
2950
|
+
},
|
|
2951
|
+
{
|
|
2952
|
+
exposedContent: [
|
|
2953
|
+
"nightly codebase audit — one domain/night on a 7-day rotation; opens a PR the reviewer merges (claude -p, subscription auth)"
|
|
2954
|
+
],
|
|
2955
|
+
kind: "cron",
|
|
2956
|
+
name: "cron.audit",
|
|
2957
|
+
operatorNotes: "01:00 Amsterdam, a rave-02 host systemd timer (docs/agents/hermes/audit-timer/). A full agentic claude -p session: audits the day's domain, fixes what's safe, files the rest to docs/audit-backlog.md, opens a PR. Subscription auth (CLAUDE_CODE_OAUTH_TOKEN), zero OpenRouter tokens. Source: docs/agents/hermes/scripts/audit-sweep.sh. Probed on /status as service `cron.audit`.",
|
|
2958
|
+
probeConfig: {
|
|
2959
|
+
cadenceMs: 24 * 60 * MINUTE_MS,
|
|
2960
|
+
cronName: "fluncle-audit",
|
|
2961
|
+
kind: "cron",
|
|
2962
|
+
schedule: { time: "01:00", tz: "Europe/Amsterdam" }
|
|
2963
|
+
},
|
|
2964
|
+
weights: { status: "secondary" }
|
|
2965
|
+
},
|
|
2966
|
+
{
|
|
2967
|
+
exposedContent: [
|
|
2968
|
+
"05:00 reviewer for the nightly audit PR — fix-small-and-merge on green CI, else comment + hold (claude -p)"
|
|
2969
|
+
],
|
|
2970
|
+
kind: "cron",
|
|
2971
|
+
name: "cron.audit-review",
|
|
2972
|
+
operatorNotes: "05:00 Amsterdam, a rave-02 host systemd timer (docs/agents/hermes/audit-review-timer/). Reviews the newest open audit/* PR adversarially; merges when required checks are green and nothing high-impact remains, else comments and leaves it for the operator. Source: docs/agents/hermes/scripts/audit-review-sweep.sh. Probed on /status as service `cron.audit-review`.",
|
|
2973
|
+
probeConfig: {
|
|
2974
|
+
cadenceMs: 24 * 60 * MINUTE_MS,
|
|
2975
|
+
cronName: "fluncle-audit-review",
|
|
2976
|
+
kind: "cron",
|
|
2977
|
+
schedule: { time: "05:00", tz: "Europe/Amsterdam" }
|
|
2978
|
+
},
|
|
2943
2979
|
weights: { status: "secondary" }
|
|
2944
2980
|
}
|
|
2945
2981
|
];
|
|
@@ -3052,7 +3088,7 @@ function checkBundleCompleteness(files) {
|
|
|
3052
3088
|
const missingFrom = (specs) => uploadingFootage ? specs.filter((spec) => !files[spec.option]).map((spec) => spec.file) : [];
|
|
3053
3089
|
const plateWarnings = [];
|
|
3054
3090
|
if (files.plateBackground && !files.plate) {
|
|
3055
|
-
plateWarnings.push("plate.background.png without plate.png
|
|
3091
|
+
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");
|
|
3056
3092
|
}
|
|
3057
3093
|
return {
|
|
3058
3094
|
missingAdvisory: missingFrom(RERENDER_ADVISORY_FIELDS),
|
|
@@ -3068,10 +3104,10 @@ async function trackVideoCommand(idOrLogId, files, onProgress, options = {}) {
|
|
|
3068
3104
|
}
|
|
3069
3105
|
if (completeness.uploadingFootage) {
|
|
3070
3106
|
if (completeness.missingContract.length > 0) {
|
|
3071
|
-
onProgress?.(`warning: --allow-partial
|
|
3107
|
+
onProgress?.(`warning: --allow-partial, uploading WITHOUT the re-render contract (${completeness.missingContract.join(", ")}); the R2 bundle will NOT be re-renderable`);
|
|
3072
3108
|
}
|
|
3073
3109
|
for (const missing of completeness.missingAdvisory) {
|
|
3074
|
-
onProgress?.(`warning: ${missing} missing (provenance/eval only)
|
|
3110
|
+
onProgress?.(`warning: ${missing} missing (provenance/eval only), shipping without it`);
|
|
3075
3111
|
}
|
|
3076
3112
|
}
|
|
3077
3113
|
for (const warning of completeness.plateWarnings) {
|
|
@@ -3106,7 +3142,7 @@ async function trackVideoCommand(idOrLogId, files, onProgress, options = {}) {
|
|
|
3106
3142
|
urls[spec.field] = `${FOUND_BASE}/${upload.key}`;
|
|
3107
3143
|
}
|
|
3108
3144
|
if (platesOnly) {
|
|
3109
|
-
onProgress?.(`plate pre-upload complete
|
|
3145
|
+
onProgress?.(`plate pre-upload complete. Compose against ${FOUND_BASE}/${presign.logId}/plate.png; finalize is deferred to the footage ship`);
|
|
3110
3146
|
return { logId: presign.logId, ok: true, trackId: presign.trackId, urls };
|
|
3111
3147
|
}
|
|
3112
3148
|
const manifest = files.render ? await readManifestFields(files.render) : {};
|
|
@@ -3264,246 +3300,6 @@ var init_track = __esm(() => {
|
|
|
3264
3300
|
NON_FILE_OPTIONS = ["model", "reasoning"];
|
|
3265
3301
|
});
|
|
3266
3302
|
|
|
3267
|
-
// src/commands/helm.ts
|
|
3268
|
-
var exports_helm = {};
|
|
3269
|
-
__export(exports_helm, {
|
|
3270
|
-
resolveHelmDir: () => resolveHelmDir,
|
|
3271
|
-
helmUninstallCommand: () => helmUninstallCommand,
|
|
3272
|
-
helmOpenCommand: () => helmOpenCommand,
|
|
3273
|
-
helmInstallCommand: () => helmInstallCommand,
|
|
3274
|
-
buildLaunchAgentPlist: () => buildLaunchAgentPlist
|
|
3275
|
-
});
|
|
3276
|
-
import { existsSync, mkdirSync as mkdirSync2, openSync, rmSync as rmSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
3277
|
-
import { homedir as homedir4 } from "node:os";
|
|
3278
|
-
import { dirname as dirname3, join as join4, resolve } from "node:path";
|
|
3279
|
-
function launchAgentPlistPath() {
|
|
3280
|
-
return join4(homedir4(), "Library/LaunchAgents", `${LAUNCH_AGENT_LABEL}.plist`);
|
|
3281
|
-
}
|
|
3282
|
-
function helmLogPath() {
|
|
3283
|
-
return join4(homedir4(), "Library/Logs/fluncle-helm.log");
|
|
3284
|
-
}
|
|
3285
|
-
function walkUpForHelm(start) {
|
|
3286
|
-
let dir = start;
|
|
3287
|
-
for (let depth = 0;depth < 8; depth++) {
|
|
3288
|
-
const candidate = join4(dir, "apps/helm");
|
|
3289
|
-
if (existsSync(join4(candidate, "src/server.ts"))) {
|
|
3290
|
-
return candidate;
|
|
3291
|
-
}
|
|
3292
|
-
const parent = dirname3(dir);
|
|
3293
|
-
if (parent === dir) {
|
|
3294
|
-
break;
|
|
3295
|
-
}
|
|
3296
|
-
dir = parent;
|
|
3297
|
-
}
|
|
3298
|
-
return null;
|
|
3299
|
-
}
|
|
3300
|
-
function persistHelmDir(dir) {
|
|
3301
|
-
try {
|
|
3302
|
-
mkdirSync2(dirname3(HELM_DIR_FILE), { recursive: true });
|
|
3303
|
-
writeFileSync2(HELM_DIR_FILE, `${dir}
|
|
3304
|
-
`, { mode: 384 });
|
|
3305
|
-
} catch {}
|
|
3306
|
-
}
|
|
3307
|
-
function resolveHelmDir() {
|
|
3308
|
-
const fromEnv = process.env.FLUNCLE_HELM_DIR;
|
|
3309
|
-
if (fromEnv) {
|
|
3310
|
-
if (!existsSync(join4(fromEnv, "src/server.ts"))) {
|
|
3311
|
-
throw new CliError2("helm_not_found", `FLUNCLE_HELM_DIR points at ${fromEnv}, but there's no helm there (src/server.ts missing).`);
|
|
3312
|
-
}
|
|
3313
|
-
return resolve(fromEnv);
|
|
3314
|
-
}
|
|
3315
|
-
const found = walkUpForHelm(process.cwd()) ?? walkUpForHelm(import.meta.dir);
|
|
3316
|
-
if (found) {
|
|
3317
|
-
persistHelmDir(found);
|
|
3318
|
-
return found;
|
|
3319
|
-
}
|
|
3320
|
-
try {
|
|
3321
|
-
const remembered = readFileSync2(HELM_DIR_FILE, "utf8").trim();
|
|
3322
|
-
if (remembered && existsSync(join4(remembered, "src/server.ts"))) {
|
|
3323
|
-
return remembered;
|
|
3324
|
-
}
|
|
3325
|
-
} catch {}
|
|
3326
|
-
throw new CliError2("helm_not_found", "No helm aboard. Run this once from the fluncle repo (the helm remembers the way after that), or set FLUNCLE_HELM_DIR.");
|
|
3327
|
-
}
|
|
3328
|
-
function resolveBun() {
|
|
3329
|
-
const bun = Bun.which("bun");
|
|
3330
|
-
if (!bun) {
|
|
3331
|
-
throw new CliError2("no_bun", "No bun on the PATH. The helm daemon runs under bun.");
|
|
3332
|
-
}
|
|
3333
|
-
return bun;
|
|
3334
|
-
}
|
|
3335
|
-
async function daemonHealthy() {
|
|
3336
|
-
try {
|
|
3337
|
-
const response = await fetch(`${HELM_URL}/api/health`, {
|
|
3338
|
-
signal: AbortSignal.timeout(1500)
|
|
3339
|
-
});
|
|
3340
|
-
return response.ok;
|
|
3341
|
-
} catch {
|
|
3342
|
-
return false;
|
|
3343
|
-
}
|
|
3344
|
-
}
|
|
3345
|
-
async function ensureBuilt(helmDir) {
|
|
3346
|
-
if (existsSync(join4(helmDir, "dist/index.html"))) {
|
|
3347
|
-
return;
|
|
3348
|
-
}
|
|
3349
|
-
console.log("The glass isn't built yet. Building it once…");
|
|
3350
|
-
const proc = Bun.spawn([resolveBun(), "run", "build"], {
|
|
3351
|
-
cwd: helmDir,
|
|
3352
|
-
stderr: "inherit",
|
|
3353
|
-
stdin: "ignore",
|
|
3354
|
-
stdout: "inherit"
|
|
3355
|
-
});
|
|
3356
|
-
const exitCode = await proc.exited;
|
|
3357
|
-
if (exitCode !== 0) {
|
|
3358
|
-
throw new CliError2("helm_build_failed", `The glass build failed (exit ${exitCode}).`);
|
|
3359
|
-
}
|
|
3360
|
-
}
|
|
3361
|
-
function bootDaemon(helmDir) {
|
|
3362
|
-
const logPath = helmLogPath();
|
|
3363
|
-
mkdirSync2(dirname3(logPath), { recursive: true });
|
|
3364
|
-
const logFd = openSync(logPath, "a");
|
|
3365
|
-
const proc = Bun.spawn([resolveBun(), "src/server.ts"], {
|
|
3366
|
-
cwd: helmDir,
|
|
3367
|
-
stderr: logFd,
|
|
3368
|
-
stdin: "ignore",
|
|
3369
|
-
stdout: logFd
|
|
3370
|
-
});
|
|
3371
|
-
proc.unref();
|
|
3372
|
-
}
|
|
3373
|
-
async function waitForHealth(timeoutMs) {
|
|
3374
|
-
const deadline = Date.now() + timeoutMs;
|
|
3375
|
-
while (Date.now() < deadline) {
|
|
3376
|
-
if (await daemonHealthy()) {
|
|
3377
|
-
return true;
|
|
3378
|
-
}
|
|
3379
|
-
await Bun.sleep(250);
|
|
3380
|
-
}
|
|
3381
|
-
return false;
|
|
3382
|
-
}
|
|
3383
|
-
async function openAppWindow() {
|
|
3384
|
-
if (process.platform === "darwin" && chromeInstalled()) {
|
|
3385
|
-
const proc = Bun.spawn(["open", "-na", "Google Chrome", "--args", `--app=${HELM_URL}`], {
|
|
3386
|
-
stderr: "ignore",
|
|
3387
|
-
stdin: "ignore",
|
|
3388
|
-
stdout: "ignore"
|
|
3389
|
-
});
|
|
3390
|
-
if (await proc.exited === 0) {
|
|
3391
|
-
return;
|
|
3392
|
-
}
|
|
3393
|
-
}
|
|
3394
|
-
await openExternal(HELM_URL);
|
|
3395
|
-
}
|
|
3396
|
-
function chromeInstalled() {
|
|
3397
|
-
return existsSync("/Applications/Google Chrome.app") || existsSync(join4(homedir4(), "Applications/Google Chrome.app"));
|
|
3398
|
-
}
|
|
3399
|
-
async function helmOpenCommand() {
|
|
3400
|
-
if (await daemonHealthy()) {
|
|
3401
|
-
console.log(`The helm holds on :${HELM_PORT}. Opening the window.`);
|
|
3402
|
-
await openAppWindow();
|
|
3403
|
-
return;
|
|
3404
|
-
}
|
|
3405
|
-
const helmDir = resolveHelmDir();
|
|
3406
|
-
await ensureBuilt(helmDir);
|
|
3407
|
-
bootDaemon(helmDir);
|
|
3408
|
-
if (!await waitForHealth(15000)) {
|
|
3409
|
-
throw new CliError2("helm_no_answer", `The daemon didn't answer on :${HELM_PORT}. Its log is at ${helmLogPath()}.`);
|
|
3410
|
-
}
|
|
3411
|
-
console.log(`Helm raised on :${HELM_PORT}. Opening the window.`);
|
|
3412
|
-
await openAppWindow();
|
|
3413
|
-
}
|
|
3414
|
-
function xmlEscape(text) {
|
|
3415
|
-
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
3416
|
-
}
|
|
3417
|
-
function buildLaunchAgentPlist(helmDir, bunPath, logPath) {
|
|
3418
|
-
const path2 = `${dirname3(bunPath)}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`;
|
|
3419
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3420
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3421
|
-
<plist version="1.0">
|
|
3422
|
-
<dict>
|
|
3423
|
-
<key>EnvironmentVariables</key>
|
|
3424
|
-
<dict>
|
|
3425
|
-
<key>PATH</key>
|
|
3426
|
-
<string>${xmlEscape(path2)}</string>
|
|
3427
|
-
</dict>
|
|
3428
|
-
<key>KeepAlive</key>
|
|
3429
|
-
<dict>
|
|
3430
|
-
<key>SuccessfulExit</key>
|
|
3431
|
-
<false/>
|
|
3432
|
-
</dict>
|
|
3433
|
-
<key>Label</key>
|
|
3434
|
-
<string>${LAUNCH_AGENT_LABEL}</string>
|
|
3435
|
-
<key>ProgramArguments</key>
|
|
3436
|
-
<array>
|
|
3437
|
-
<string>${xmlEscape(bunPath)}</string>
|
|
3438
|
-
<string>src/server.ts</string>
|
|
3439
|
-
</array>
|
|
3440
|
-
<key>RunAtLoad</key>
|
|
3441
|
-
<true/>
|
|
3442
|
-
<key>StandardErrorPath</key>
|
|
3443
|
-
<string>${xmlEscape(logPath)}</string>
|
|
3444
|
-
<key>StandardOutPath</key>
|
|
3445
|
-
<string>${xmlEscape(logPath)}</string>
|
|
3446
|
-
<key>WorkingDirectory</key>
|
|
3447
|
-
<string>${xmlEscape(helmDir)}</string>
|
|
3448
|
-
</dict>
|
|
3449
|
-
</plist>
|
|
3450
|
-
`;
|
|
3451
|
-
}
|
|
3452
|
-
function currentUid() {
|
|
3453
|
-
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
3454
|
-
if (uid === undefined) {
|
|
3455
|
-
throw new CliError2("no_uid", "Couldn't read the user id — launchd needs a gui domain.");
|
|
3456
|
-
}
|
|
3457
|
-
return uid;
|
|
3458
|
-
}
|
|
3459
|
-
async function launchctl(args) {
|
|
3460
|
-
const proc = Bun.spawn(["launchctl", ...args], {
|
|
3461
|
-
stderr: "ignore",
|
|
3462
|
-
stdin: "ignore",
|
|
3463
|
-
stdout: "ignore"
|
|
3464
|
-
});
|
|
3465
|
-
return proc.exited;
|
|
3466
|
-
}
|
|
3467
|
-
async function helmInstallCommand() {
|
|
3468
|
-
if (process.platform !== "darwin") {
|
|
3469
|
-
throw new CliError2("unsupported_platform", "launchd is a macOS thing. Nothing to install here.");
|
|
3470
|
-
}
|
|
3471
|
-
const helmDir = resolveHelmDir();
|
|
3472
|
-
await ensureBuilt(helmDir);
|
|
3473
|
-
const plistPath = launchAgentPlistPath();
|
|
3474
|
-
mkdirSync2(dirname3(plistPath), { recursive: true });
|
|
3475
|
-
mkdirSync2(dirname3(helmLogPath()), { recursive: true });
|
|
3476
|
-
writeFileSync2(plistPath, buildLaunchAgentPlist(helmDir, resolveBun(), helmLogPath()));
|
|
3477
|
-
const uid = currentUid();
|
|
3478
|
-
await launchctl(["bootout", `gui/${uid}/${LAUNCH_AGENT_LABEL}`]);
|
|
3479
|
-
const exitCode = await launchctl(["bootstrap", `gui/${uid}`, plistPath]);
|
|
3480
|
-
if (exitCode !== 0) {
|
|
3481
|
-
throw new CliError2("launchctl_refused", `launchctl bootstrap refused (exit ${exitCode}). The plist is at ${plistPath}.`);
|
|
3482
|
-
}
|
|
3483
|
-
console.log(`LaunchAgent installed (${LAUNCH_AGENT_LABEL}).`);
|
|
3484
|
-
console.log(`The daemon rises at login and holds :${HELM_PORT}. Log: ${helmLogPath()}`);
|
|
3485
|
-
}
|
|
3486
|
-
async function helmUninstallCommand() {
|
|
3487
|
-
if (process.platform !== "darwin") {
|
|
3488
|
-
throw new CliError2("unsupported_platform", "launchd is a macOS thing. Nothing to remove here.");
|
|
3489
|
-
}
|
|
3490
|
-
const plistPath = launchAgentPlistPath();
|
|
3491
|
-
await launchctl(["bootout", `gui/${currentUid()}/${LAUNCH_AGENT_LABEL}`]);
|
|
3492
|
-
if (existsSync(plistPath)) {
|
|
3493
|
-
rmSync2(plistPath);
|
|
3494
|
-
console.log("LaunchAgent removed and the daemon stood down.");
|
|
3495
|
-
return;
|
|
3496
|
-
}
|
|
3497
|
-
console.log("No LaunchAgent on file. Nothing stood down.");
|
|
3498
|
-
}
|
|
3499
|
-
var HELM_PORT = 4190, HELM_URL, LAUNCH_AGENT_LABEL = "com.fluncle.helm", HELM_DIR_FILE;
|
|
3500
|
-
var init_helm = __esm(() => {
|
|
3501
|
-
init_open_external();
|
|
3502
|
-
init_output();
|
|
3503
|
-
HELM_URL = `http://127.0.0.1:${HELM_PORT}`;
|
|
3504
|
-
HELM_DIR_FILE = join4(homedir4(), ".config/fluncle/helm-dir");
|
|
3505
|
-
});
|
|
3506
|
-
|
|
3507
3303
|
// src/commands/add.ts
|
|
3508
3304
|
var exports_add = {};
|
|
3509
3305
|
__export(exports_add, {
|
|
@@ -3925,12 +3721,12 @@ async function distributeYoutube2(mixtapeId, videoPath, onProgress) {
|
|
|
3925
3721
|
continue;
|
|
3926
3722
|
}
|
|
3927
3723
|
if (result.kind === "reauth") {
|
|
3928
|
-
onProgress?.("Access token expired
|
|
3724
|
+
onProgress?.("Access token expired, re-minting and resuming");
|
|
3929
3725
|
session = { accessToken: await mintToken2(), sessionUri: session.sessionUri };
|
|
3930
3726
|
offset = await queryOffset2(session.sessionUri, session.accessToken, contentLength);
|
|
3931
3727
|
continue;
|
|
3932
3728
|
}
|
|
3933
|
-
onProgress?.("Upload session expired
|
|
3729
|
+
onProgress?.("Upload session expired, re-initiating");
|
|
3934
3730
|
session = await initiate2(mixtapeId, contentLength, contentType);
|
|
3935
3731
|
offset = 0;
|
|
3936
3732
|
}
|
|
@@ -4085,9 +3881,9 @@ var init_multipart = __esm(() => {
|
|
|
4085
3881
|
|
|
4086
3882
|
// src/commands/mixtape-set-video.ts
|
|
4087
3883
|
import { randomUUID } from "node:crypto";
|
|
4088
|
-
import { existsSync
|
|
3884
|
+
import { existsSync, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
|
|
4089
3885
|
import { tmpdir } from "node:os";
|
|
4090
|
-
import { join as
|
|
3886
|
+
import { join as join4 } from "node:path";
|
|
4091
3887
|
function renditionFfmpegArgs(inputPath, outputPath) {
|
|
4092
3888
|
return [
|
|
4093
3889
|
"-y",
|
|
@@ -4115,11 +3911,11 @@ function renditionFfmpegArgs(inputPath, outputPath) {
|
|
|
4115
3911
|
];
|
|
4116
3912
|
}
|
|
4117
3913
|
async function uploadRenditionMultipart(masterPath, presignPath, onProgress = () => {}) {
|
|
4118
|
-
if (!
|
|
3914
|
+
if (!existsSync(masterPath)) {
|
|
4119
3915
|
throw new CliError2("file_not_found", `Set-video master not found: ${masterPath}`);
|
|
4120
3916
|
}
|
|
4121
3917
|
await assertFfmpeg();
|
|
4122
|
-
const renditionPath =
|
|
3918
|
+
const renditionPath = join4(tmpdir(), `fluncle-set-${randomUUID()}.mp4`);
|
|
4123
3919
|
onProgress("Set video: deriving the 1080p faststart rendition (ffmpeg)…");
|
|
4124
3920
|
await deriveRendition(masterPath, renditionPath);
|
|
4125
3921
|
try {
|
|
@@ -4148,7 +3944,7 @@ async function uploadRenditionMultipart(masterPath, presignPath, onProgress = ()
|
|
|
4148
3944
|
}
|
|
4149
3945
|
return { key: presign.key, url: `${FOUND_BASE2}/${presign.key}` };
|
|
4150
3946
|
} finally {
|
|
4151
|
-
|
|
3947
|
+
rmSync2(renditionPath, { force: true });
|
|
4152
3948
|
}
|
|
4153
3949
|
}
|
|
4154
3950
|
async function putPart(url, path2, part, onProgress = () => {}) {
|
|
@@ -4164,8 +3960,8 @@ async function putPart(url, path2, part, onProgress = () => {}) {
|
|
|
4164
3960
|
throw new CliError2("r2_part_failed", `Part ${part.partNumber} failed after ${MAX_PART_ATTEMPTS} attempts: ${error instanceof Error ? error.message : String(error)}`);
|
|
4165
3961
|
}
|
|
4166
3962
|
const backoffMs = 500 * 2 ** (attempt - 1);
|
|
4167
|
-
onProgress(`Set video: part ${part.partNumber} dropped
|
|
4168
|
-
await new Promise((
|
|
3963
|
+
onProgress(`Set video: part ${part.partNumber} dropped, retry ${attempt}/${MAX_PART_ATTEMPTS - 1} in ${backoffMs}ms…`);
|
|
3964
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
4169
3965
|
}
|
|
4170
3966
|
}
|
|
4171
3967
|
}
|
|
@@ -4245,7 +4041,7 @@ __export(exports_recordings, {
|
|
|
4245
4041
|
recordingDeleteCommand: () => recordingDeleteCommand,
|
|
4246
4042
|
recordingCreateCommand: () => recordingCreateCommand
|
|
4247
4043
|
});
|
|
4248
|
-
import { existsSync as
|
|
4044
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
4249
4045
|
async function recordingGet(id) {
|
|
4250
4046
|
const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
4251
4047
|
return response.recording;
|
|
@@ -4304,7 +4100,7 @@ async function recordingCreateCommand(options = {}) {
|
|
|
4304
4100
|
printJson2({ ok: true, recording: recording2 });
|
|
4305
4101
|
return;
|
|
4306
4102
|
}
|
|
4307
|
-
console.log(`Plan ${recording2.id} created
|
|
4103
|
+
console.log(`Plan ${recording2.id} created, handle "${recording2.title}"`);
|
|
4308
4104
|
return;
|
|
4309
4105
|
}
|
|
4310
4106
|
const title = options.title?.trim();
|
|
@@ -4314,7 +4110,7 @@ async function recordingCreateCommand(options = {}) {
|
|
|
4314
4110
|
if (!options.video) {
|
|
4315
4111
|
throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
|
|
4316
4112
|
}
|
|
4317
|
-
if (!
|
|
4113
|
+
if (!existsSync2(options.video)) {
|
|
4318
4114
|
throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
|
|
4319
4115
|
}
|
|
4320
4116
|
const created = await adminApiPost("/api/admin/recordings", {
|
|
@@ -4327,7 +4123,7 @@ async function recordingCreateCommand(options = {}) {
|
|
|
4327
4123
|
console.log(message);
|
|
4328
4124
|
}
|
|
4329
4125
|
};
|
|
4330
|
-
log(`Recording ${recording.id} created
|
|
4126
|
+
log(`Recording ${recording.id} created, staging the set video…`);
|
|
4331
4127
|
const upload = await uploadRenditionMultipart(options.video, `/api/admin/recordings/${encodeURIComponent(recording.id)}/set-video/presign`, log);
|
|
4332
4128
|
if (options.json) {
|
|
4333
4129
|
printJson2({ key: upload.key, ok: true, recording, url: upload.url });
|
|
@@ -4350,17 +4146,17 @@ async function recordingUpdateCommand(id, options = {}) {
|
|
|
4350
4146
|
body.parentId = options.parentId;
|
|
4351
4147
|
}
|
|
4352
4148
|
if (options.tracklistFile !== undefined) {
|
|
4353
|
-
if (!
|
|
4149
|
+
if (!existsSync2(options.tracklistFile)) {
|
|
4354
4150
|
throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
|
|
4355
4151
|
}
|
|
4356
4152
|
try {
|
|
4357
|
-
body.tracklistJson = JSON.parse(
|
|
4153
|
+
body.tracklistJson = JSON.parse(readFileSync2(options.tracklistFile, "utf8"));
|
|
4358
4154
|
} catch {
|
|
4359
4155
|
throw new CliError2("invalid_tracklist", `Tracklist file is not valid JSON: ${options.tracklistFile}`);
|
|
4360
4156
|
}
|
|
4361
4157
|
}
|
|
4362
4158
|
if (Object.keys(body).length === 0) {
|
|
4363
|
-
throw new CliError2("no_fields", "Nothing to update
|
|
4159
|
+
throw new CliError2("no_fields", "Nothing to update. Pass --title, --recorded-at, --parent-id, or --tracklist-file");
|
|
4364
4160
|
}
|
|
4365
4161
|
const response = await adminApiPatch(`/api/admin/recordings/${encodeURIComponent(id)}`, body);
|
|
4366
4162
|
if (options.json) {
|
|
@@ -4399,12 +4195,12 @@ async function recordingReplaceCuesCommand(id, options = {}) {
|
|
|
4399
4195
|
if (!options.cuesFile) {
|
|
4400
4196
|
throw new CliError2("missing_cues_file", "replace-cues needs a --cues-file <file> (the ordered cue array)");
|
|
4401
4197
|
}
|
|
4402
|
-
if (!
|
|
4198
|
+
if (!existsSync2(options.cuesFile)) {
|
|
4403
4199
|
throw new CliError2("file_not_found", `Cues file not found: ${options.cuesFile}`);
|
|
4404
4200
|
}
|
|
4405
4201
|
let cues;
|
|
4406
4202
|
try {
|
|
4407
|
-
cues = JSON.parse(
|
|
4203
|
+
cues = JSON.parse(readFileSync2(options.cuesFile, "utf8"));
|
|
4408
4204
|
} catch {
|
|
4409
4205
|
throw new CliError2("invalid_cues", `Cues file is not valid JSON: ${options.cuesFile}`);
|
|
4410
4206
|
}
|
|
@@ -4442,9 +4238,9 @@ __export(exports_clips, {
|
|
|
4442
4238
|
CLIP_AUDIO_BITRATE: () => CLIP_AUDIO_BITRATE
|
|
4443
4239
|
});
|
|
4444
4240
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4445
|
-
import { rmSync as
|
|
4241
|
+
import { rmSync as rmSync3, statSync as statSync4 } from "node:fs";
|
|
4446
4242
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
4447
|
-
import { join as
|
|
4243
|
+
import { join as join5 } from "node:path";
|
|
4448
4244
|
function clipFootageKey(clipId) {
|
|
4449
4245
|
return `${clipId}/footage.mp4`;
|
|
4450
4246
|
}
|
|
@@ -4541,7 +4337,7 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4541
4337
|
}
|
|
4542
4338
|
const source = await resolveClipSource(clip);
|
|
4543
4339
|
await assertFfmpeg2();
|
|
4544
|
-
const outputPath =
|
|
4340
|
+
const outputPath = join5(tmpdir2(), `fluncle-clip-${randomUUID2()}.mp4`);
|
|
4545
4341
|
try {
|
|
4546
4342
|
onProgress(`Clip ${clipId}: cutting [${clip.inMs}–${clip.outMs}ms]…`);
|
|
4547
4343
|
await runClipCut({
|
|
@@ -4553,7 +4349,7 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4553
4349
|
});
|
|
4554
4350
|
const sizeBytes = statSync4(outputPath).size;
|
|
4555
4351
|
if (sizeBytes > MAX_CLIP_BYTES) {
|
|
4556
|
-
throw new CliError2("clip_too_large", `The cut is ${(sizeBytes / 1e6).toFixed(1)} MB (> 100 MB Cloudflare MT ceiling)
|
|
4352
|
+
throw new CliError2("clip_too_large", `The cut is ${(sizeBytes / 1e6).toFixed(1)} MB (> 100 MB Cloudflare MT ceiling). Shorten the window or lower the bitrate cap`);
|
|
4557
4353
|
}
|
|
4558
4354
|
onProgress(`Clip ${clipId}: uploading ${(sizeBytes / 1e6).toFixed(1)} MB…`);
|
|
4559
4355
|
const presign = await adminApiPost(`/api/admin/clips/${encodeURIComponent(clipId)}/cut/presign`, { contentType: "video/mp4" });
|
|
@@ -4567,7 +4363,7 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4567
4363
|
url: `${FOUND_BASE3}/${presign.key}`
|
|
4568
4364
|
};
|
|
4569
4365
|
} finally {
|
|
4570
|
-
|
|
4366
|
+
rmSync3(outputPath, { force: true });
|
|
4571
4367
|
}
|
|
4572
4368
|
}
|
|
4573
4369
|
async function putClip(url, contentType, path2) {
|
|
@@ -4625,7 +4421,7 @@ __export(exports_recordings2, {
|
|
|
4625
4421
|
recordingDeleteCommand: () => recordingDeleteCommand2,
|
|
4626
4422
|
recordingCreateCommand: () => recordingCreateCommand2
|
|
4627
4423
|
});
|
|
4628
|
-
import { existsSync as
|
|
4424
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
4629
4425
|
async function recordingGet2(id) {
|
|
4630
4426
|
const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
4631
4427
|
return response.recording;
|
|
@@ -4684,7 +4480,7 @@ async function recordingCreateCommand2(options = {}) {
|
|
|
4684
4480
|
printJson2({ ok: true, recording: recording2 });
|
|
4685
4481
|
return;
|
|
4686
4482
|
}
|
|
4687
|
-
console.log(`Plan ${recording2.id} created
|
|
4483
|
+
console.log(`Plan ${recording2.id} created, handle "${recording2.title}"`);
|
|
4688
4484
|
return;
|
|
4689
4485
|
}
|
|
4690
4486
|
const title = options.title?.trim();
|
|
@@ -4694,7 +4490,7 @@ async function recordingCreateCommand2(options = {}) {
|
|
|
4694
4490
|
if (!options.video) {
|
|
4695
4491
|
throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
|
|
4696
4492
|
}
|
|
4697
|
-
if (!
|
|
4493
|
+
if (!existsSync3(options.video)) {
|
|
4698
4494
|
throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
|
|
4699
4495
|
}
|
|
4700
4496
|
const created = await adminApiPost("/api/admin/recordings", {
|
|
@@ -4707,7 +4503,7 @@ async function recordingCreateCommand2(options = {}) {
|
|
|
4707
4503
|
console.log(message);
|
|
4708
4504
|
}
|
|
4709
4505
|
};
|
|
4710
|
-
log(`Recording ${recording.id} created
|
|
4506
|
+
log(`Recording ${recording.id} created, staging the set video…`);
|
|
4711
4507
|
const upload = await uploadRenditionMultipart(options.video, `/api/admin/recordings/${encodeURIComponent(recording.id)}/set-video/presign`, log);
|
|
4712
4508
|
if (options.json) {
|
|
4713
4509
|
printJson2({ key: upload.key, ok: true, recording, url: upload.url });
|
|
@@ -4730,17 +4526,17 @@ async function recordingUpdateCommand2(id, options = {}) {
|
|
|
4730
4526
|
body.parentId = options.parentId;
|
|
4731
4527
|
}
|
|
4732
4528
|
if (options.tracklistFile !== undefined) {
|
|
4733
|
-
if (!
|
|
4529
|
+
if (!existsSync3(options.tracklistFile)) {
|
|
4734
4530
|
throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
|
|
4735
4531
|
}
|
|
4736
4532
|
try {
|
|
4737
|
-
body.tracklistJson = JSON.parse(
|
|
4533
|
+
body.tracklistJson = JSON.parse(readFileSync3(options.tracklistFile, "utf8"));
|
|
4738
4534
|
} catch {
|
|
4739
4535
|
throw new CliError2("invalid_tracklist", `Tracklist file is not valid JSON: ${options.tracklistFile}`);
|
|
4740
4536
|
}
|
|
4741
4537
|
}
|
|
4742
4538
|
if (Object.keys(body).length === 0) {
|
|
4743
|
-
throw new CliError2("no_fields", "Nothing to update
|
|
4539
|
+
throw new CliError2("no_fields", "Nothing to update. Pass --title, --recorded-at, --parent-id, or --tracklist-file");
|
|
4744
4540
|
}
|
|
4745
4541
|
const response = await adminApiPatch(`/api/admin/recordings/${encodeURIComponent(id)}`, body);
|
|
4746
4542
|
if (options.json) {
|
|
@@ -4779,12 +4575,12 @@ async function recordingReplaceCuesCommand2(id, options = {}) {
|
|
|
4779
4575
|
if (!options.cuesFile) {
|
|
4780
4576
|
throw new CliError2("missing_cues_file", "replace-cues needs a --cues-file <file> (the ordered cue array)");
|
|
4781
4577
|
}
|
|
4782
|
-
if (!
|
|
4578
|
+
if (!existsSync3(options.cuesFile)) {
|
|
4783
4579
|
throw new CliError2("file_not_found", `Cues file not found: ${options.cuesFile}`);
|
|
4784
4580
|
}
|
|
4785
4581
|
let cues;
|
|
4786
4582
|
try {
|
|
4787
|
-
cues = JSON.parse(
|
|
4583
|
+
cues = JSON.parse(readFileSync3(options.cuesFile, "utf8"));
|
|
4788
4584
|
} catch {
|
|
4789
4585
|
throw new CliError2("invalid_cues", `Cues file is not valid JSON: ${options.cuesFile}`);
|
|
4790
4586
|
}
|
|
@@ -4810,7 +4606,7 @@ __export(exports_newsletter, {
|
|
|
4810
4606
|
newsletterDraftCommand: () => newsletterDraftCommand,
|
|
4811
4607
|
newsletterDeleteCommand: () => newsletterDeleteCommand
|
|
4812
4608
|
});
|
|
4813
|
-
import { existsSync as
|
|
4609
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
4814
4610
|
function buildBody2(options, { requireContent }) {
|
|
4815
4611
|
const body = {};
|
|
4816
4612
|
if (options.contentFile !== undefined) {
|
|
@@ -4830,10 +4626,10 @@ function buildBody2(options, { requireContent }) {
|
|
|
4830
4626
|
return body;
|
|
4831
4627
|
}
|
|
4832
4628
|
function readContentFile(filePath) {
|
|
4833
|
-
if (!
|
|
4629
|
+
if (!existsSync4(filePath)) {
|
|
4834
4630
|
throw new CliError2("file_not_found", `Content file not found: ${filePath}`);
|
|
4835
4631
|
}
|
|
4836
|
-
const text =
|
|
4632
|
+
const text = readFileSync4(filePath, "utf-8");
|
|
4837
4633
|
try {
|
|
4838
4634
|
return JSON.parse(text);
|
|
4839
4635
|
} catch (error) {
|
|
@@ -5217,6 +5013,26 @@ var init_admin_artists = __esm(() => {
|
|
|
5217
5013
|
init_api();
|
|
5218
5014
|
});
|
|
5219
5015
|
|
|
5016
|
+
// src/commands/preview-migration.ts
|
|
5017
|
+
var exports_preview_migration = {};
|
|
5018
|
+
__export(exports_preview_migration, {
|
|
5019
|
+
migratePreviewArchiveCommand: () => migratePreviewArchiveCommand
|
|
5020
|
+
});
|
|
5021
|
+
async function migratePreviewArchiveCommand(args) {
|
|
5022
|
+
const params = new URLSearchParams({
|
|
5023
|
+
dryRun: String(args.dryRun),
|
|
5024
|
+
limit: String(args.limit),
|
|
5025
|
+
mode: args.mode
|
|
5026
|
+
});
|
|
5027
|
+
if (args.cursor) {
|
|
5028
|
+
params.set("cursor", args.cursor);
|
|
5029
|
+
}
|
|
5030
|
+
return adminApiPost(`/api/admin/migrations/preview-archive?${params.toString()}`);
|
|
5031
|
+
}
|
|
5032
|
+
var init_preview_migration = __esm(() => {
|
|
5033
|
+
init_api();
|
|
5034
|
+
});
|
|
5035
|
+
|
|
5220
5036
|
// src/interactive.ts
|
|
5221
5037
|
var exports_interactive = {};
|
|
5222
5038
|
__export(exports_interactive, {
|
|
@@ -5250,7 +5066,7 @@ async function selectWithKeyboard2(items, options) {
|
|
|
5250
5066
|
let renderedLines = 0;
|
|
5251
5067
|
let done = false;
|
|
5252
5068
|
const wasRaw = stdin.isRaw === true;
|
|
5253
|
-
return await new Promise((
|
|
5069
|
+
return await new Promise((resolve) => {
|
|
5254
5070
|
function cleanup() {
|
|
5255
5071
|
if (done) {
|
|
5256
5072
|
return;
|
|
@@ -5265,14 +5081,14 @@ async function selectWithKeyboard2(items, options) {
|
|
|
5265
5081
|
cleanup();
|
|
5266
5082
|
stdout.write(renderedLines > 0 ? `
|
|
5267
5083
|
` : "");
|
|
5268
|
-
|
|
5084
|
+
resolve(item);
|
|
5269
5085
|
}
|
|
5270
5086
|
function cancel() {
|
|
5271
5087
|
cleanup();
|
|
5272
5088
|
clearRendered2(stdout, renderedLines);
|
|
5273
5089
|
stdout.write(`Cancelled.
|
|
5274
5090
|
`);
|
|
5275
|
-
|
|
5091
|
+
resolve(undefined);
|
|
5276
5092
|
}
|
|
5277
5093
|
function render() {
|
|
5278
5094
|
clearRendered2(stdout, renderedLines);
|
|
@@ -5328,7 +5144,7 @@ async function paginateWithKeyboard(options) {
|
|
|
5328
5144
|
let busy = false;
|
|
5329
5145
|
let done = false;
|
|
5330
5146
|
const wasRaw = stdin.isRaw === true;
|
|
5331
|
-
return await new Promise((
|
|
5147
|
+
return await new Promise((resolve) => {
|
|
5332
5148
|
function cleanup() {
|
|
5333
5149
|
if (done) {
|
|
5334
5150
|
return;
|
|
@@ -5343,7 +5159,7 @@ async function paginateWithKeyboard(options) {
|
|
|
5343
5159
|
cleanup();
|
|
5344
5160
|
stdout.write(`
|
|
5345
5161
|
`);
|
|
5346
|
-
|
|
5162
|
+
resolve();
|
|
5347
5163
|
}
|
|
5348
5164
|
function render() {
|
|
5349
5165
|
clearRendered2(stdout, renderedLines);
|
|
@@ -5496,7 +5312,7 @@ var init_format2 = __esm(() => {
|
|
|
5496
5312
|
});
|
|
5497
5313
|
|
|
5498
5314
|
// src/cli.ts
|
|
5499
|
-
import { existsSync as
|
|
5315
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
5500
5316
|
import path2 from "path";
|
|
5501
5317
|
|
|
5502
5318
|
// ../../node_modules/commander/lib/error.js
|
|
@@ -7642,7 +7458,6 @@ ${fluncleAsciiLogo}
|
|
|
7642
7458
|
addMetaCommands(program2);
|
|
7643
7459
|
addTrackCommands(program2);
|
|
7644
7460
|
addAdminCommands(program2);
|
|
7645
|
-
addHelmCommands(program2);
|
|
7646
7461
|
return program2;
|
|
7647
7462
|
}
|
|
7648
7463
|
async function main(args = process.argv.slice(2)) {
|
|
@@ -7680,7 +7495,7 @@ function addListenCommands(program2) {
|
|
|
7680
7495
|
const { recentCommand: recentCommand3 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
|
|
7681
7496
|
await runRecent(options, recentCommand3);
|
|
7682
7497
|
});
|
|
7683
|
-
program2.command("artists").description("Browse artists in Fluncle's archive").argument("[slug]", "Artist slug
|
|
7498
|
+
program2.command("artists").description("Browse artists in Fluncle's archive").argument("[slug]", "Artist slug (omit for the full list)").option("--json", "Print JSON", false).action(async (slug, options) => {
|
|
7684
7499
|
const { artistsListCommand: artistsListCommand2, artistsGetCommand: artistsGetCommand2 } = await Promise.resolve().then(() => (init_artists(), exports_artists));
|
|
7685
7500
|
await runArtists(slug, options, { artistsGetCommand: artistsGetCommand2, artistsListCommand: artistsListCommand2 });
|
|
7686
7501
|
});
|
|
@@ -7745,21 +7560,6 @@ function addTrackCommands(program2) {
|
|
|
7745
7560
|
await runTrackGet(idOrLogId, options, trackGetCommand2);
|
|
7746
7561
|
});
|
|
7747
7562
|
}
|
|
7748
|
-
function addHelmCommands(program2) {
|
|
7749
|
-
const helm = configureCommand(program2.command("helm", { hidden: true }).description("Fluncle's Helm \u2014 the operator's mission control"));
|
|
7750
|
-
helm.action(async () => {
|
|
7751
|
-
const { helmOpenCommand: helmOpenCommand2 } = await Promise.resolve().then(() => (init_helm(), exports_helm));
|
|
7752
|
-
await helmOpenCommand2();
|
|
7753
|
-
});
|
|
7754
|
-
helm.command("install").description("Install the launchd LaunchAgent (the daemon rises at login)").action(async () => {
|
|
7755
|
-
const { helmInstallCommand: helmInstallCommand2 } = await Promise.resolve().then(() => (init_helm(), exports_helm));
|
|
7756
|
-
await helmInstallCommand2();
|
|
7757
|
-
});
|
|
7758
|
-
helm.command("uninstall").description("Remove the LaunchAgent and stand the daemon down").action(async () => {
|
|
7759
|
-
const { helmUninstallCommand: helmUninstallCommand2 } = await Promise.resolve().then(() => (init_helm(), exports_helm));
|
|
7760
|
-
await helmUninstallCommand2();
|
|
7761
|
-
});
|
|
7762
|
-
}
|
|
7763
7563
|
function addAdminCommands(program2) {
|
|
7764
7564
|
const admin = configureCommand(program2.command("admin", { hidden: true }).description("Operator commands"));
|
|
7765
7565
|
admin.action(() => {
|
|
@@ -7784,27 +7584,27 @@ function addAdminCommands(program2) {
|
|
|
7784
7584
|
const { listCommand: listCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
7785
7585
|
await runAdminTracksList(options, listCommand2);
|
|
7786
7586
|
});
|
|
7787
|
-
adminTracks.command("enrich").description("Enrichment worklist (pending, failed, or stuck processing)
|
|
7587
|
+
adminTracks.command("enrich").description("Enrichment worklist (pending, failed, or stuck processing). Use --queue").option("--queue", "Show the enrichment worklist, oldest first", false).option("--limit <limit>", "Number of findings to show with --queue", "10").option("--json", "Print JSON", false).action(async (options) => {
|
|
7788
7588
|
if (!options.queue) {
|
|
7789
|
-
console.error("`tracks enrich` is a worklist view
|
|
7589
|
+
console.error("`tracks enrich` is a worklist view. Enrichment runs on the on-box `fluncle-enrich` cron.\nUse `tracks enrich --queue` to see findings needing (re-)enrichment.");
|
|
7790
7590
|
process.exitCode = 1;
|
|
7791
7591
|
return;
|
|
7792
7592
|
}
|
|
7793
7593
|
const { enrichQueueCommand: enrichQueueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
7794
7594
|
await runAdminEnrichQueue(options, enrichQueueCommand2);
|
|
7795
7595
|
});
|
|
7796
|
-
adminTracks.command("embed").description("Audio-embedding worklist (findings with no MuQ vector yet)
|
|
7596
|
+
adminTracks.command("embed").description("Audio-embedding worklist (findings with no MuQ vector yet). Use --queue").option("--queue", "Show the embedding worklist, oldest first", false).option("--limit <limit>", "Number of findings to show with --queue", "10").option("--json", "Print JSON", false).action(async (options) => {
|
|
7797
7597
|
if (!options.queue) {
|
|
7798
|
-
console.error("`tracks embed` is a worklist view
|
|
7598
|
+
console.error("`tracks embed` is a worklist view. Embedding runs on the on-box `fluncle-embed` cron.\nUse `tracks embed --queue` to see findings needing an audio embedding.");
|
|
7799
7599
|
process.exitCode = 1;
|
|
7800
7600
|
return;
|
|
7801
7601
|
}
|
|
7802
7602
|
const { embedQueueCommand: embedQueueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
7803
7603
|
await runAdminEmbedQueue(options, embedQueueCommand2);
|
|
7804
7604
|
});
|
|
7805
|
-
adminTracks.command("capture-audio").description("Full-song capture worklist (findings with no source audio yet)
|
|
7605
|
+
adminTracks.command("capture-audio").description("Full-song capture worklist (findings with no source audio yet). Use --queue").option("--queue", "Show the capture worklist, newest first", false).option("--limit <limit>", "Number of findings to show with --queue", "10").option("--json", "Print JSON", false).action(async (options) => {
|
|
7806
7606
|
if (!options.queue) {
|
|
7807
|
-
console.error("`tracks capture-audio` is a worklist view
|
|
7607
|
+
console.error("`tracks capture-audio` is a worklist view. Capture runs on the on-box `fluncle-capture` cron.\nUse `tracks capture-audio --queue` to see findings needing a full-song capture.");
|
|
7808
7608
|
process.exitCode = 1;
|
|
7809
7609
|
return;
|
|
7810
7610
|
}
|
|
@@ -7896,7 +7696,7 @@ function addAdminCommands(program2) {
|
|
|
7896
7696
|
const { mixtapeGetCommand: mixtapeGetCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7897
7697
|
await runMixtapeGet(idOrLogId, options, mixtapeGetCommand2);
|
|
7898
7698
|
});
|
|
7899
|
-
adminMixtapes.command("distribute").description("Push a promoted mixtape to YouTube (video) and Mixcloud (audio). The mixtape must already be promoted (`recordings promote`)
|
|
7699
|
+
adminMixtapes.command("distribute").description("Push a promoted mixtape to YouTube (video) and Mixcloud (audio). The mixtape must already be promoted (`recordings promote`). Distribute is push-only.").argument("[idOrLogId]").option("--video <file>", "Video file for YouTube").option("--audio <file>", "Audio file for Mixcloud").option("--youtube", "Only distribute to YouTube").option("--mixcloud", "Only distribute to Mixcloud").option("--unlisted", "Keep Mixcloud private too (YouTube is always unlisted until publish-youtube)").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
|
|
7900
7700
|
const { mixtapeDistributeCommand: mixtapeDistributeCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7901
7701
|
await runMixtapeDistribute(idOrLogId, options, mixtapeDistributeCommand2);
|
|
7902
7702
|
});
|
|
@@ -7920,11 +7720,11 @@ function addAdminCommands(program2) {
|
|
|
7920
7720
|
const { clipScheduleCommand: clipScheduleCommand2 } = await Promise.resolve().then(() => (init_clips(), exports_clips));
|
|
7921
7721
|
await runClipsSchedule(clipId, options, clipScheduleCommand2);
|
|
7922
7722
|
});
|
|
7923
|
-
adminClips.command("drip-pause").description("Pause the whole Instagram drip-feed
|
|
7723
|
+
adminClips.command("drip-pause").description("Pause the whole Instagram drip-feed, the kill switch (operator)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
7924
7724
|
const { clipDripPauseCommand: clipDripPauseCommand2 } = await Promise.resolve().then(() => (init_clips(), exports_clips));
|
|
7925
7725
|
await runClipsDripPause(true, options, clipDripPauseCommand2);
|
|
7926
7726
|
});
|
|
7927
|
-
adminClips.command("drip-resume").description("Resume the Instagram drip-feed
|
|
7727
|
+
adminClips.command("drip-resume").description("Resume the Instagram drip-feed, clear the kill switch (operator)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
7928
7728
|
const { clipDripPauseCommand: clipDripPauseCommand2 } = await Promise.resolve().then(() => (init_clips(), exports_clips));
|
|
7929
7729
|
await runClipsDripPause(false, options, clipDripPauseCommand2);
|
|
7930
7730
|
});
|
|
@@ -7976,7 +7776,7 @@ function addAdminCommands(program2) {
|
|
|
7976
7776
|
const { newsletterUpdateCommand: newsletterUpdateCommand2 } = await Promise.resolve().then(() => (init_newsletter(), exports_newsletter));
|
|
7977
7777
|
await runNewsletterUpdate(id, options, newsletterUpdateCommand2);
|
|
7978
7778
|
});
|
|
7979
|
-
adminNewsletter.command("send").description("Send an edition
|
|
7779
|
+
adminNewsletter.command("send").description("Send an edition, OPERATOR only (Resend broadcast + mint the number)").argument("[id]").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
|
|
7980
7780
|
const { newsletterSendCommand: newsletterSendCommand2 } = await Promise.resolve().then(() => (init_newsletter(), exports_newsletter));
|
|
7981
7781
|
await runNewsletterSend(id, options, newsletterSendCommand2);
|
|
7982
7782
|
});
|
|
@@ -7984,7 +7784,7 @@ function addAdminCommands(program2) {
|
|
|
7984
7784
|
const { newsletterListCommand: newsletterListCommand2 } = await Promise.resolve().then(() => (init_newsletter(), exports_newsletter));
|
|
7985
7785
|
await runNewsletterList(options, newsletterListCommand2);
|
|
7986
7786
|
});
|
|
7987
|
-
adminNewsletter.command("delete").description("Delete an edition (draft or sent)
|
|
7787
|
+
adminNewsletter.command("delete").description("Delete an edition (draft or sent). OPERATOR only; reopens the send window").argument("[id]").option("--yes", "Confirm the delete", false).option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
|
|
7988
7788
|
const { newsletterDeleteCommand: newsletterDeleteCommand2 } = await Promise.resolve().then(() => (init_newsletter(), exports_newsletter));
|
|
7989
7789
|
await runNewsletterDelete(id, options, newsletterDeleteCommand2);
|
|
7990
7790
|
});
|
|
@@ -8064,6 +7864,14 @@ function addAdminCommands(program2) {
|
|
|
8064
7864
|
const { resolveArtistCommand: resolveArtistCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
|
|
8065
7865
|
await runArtistResolve(artistId, options, resolveArtistCommand2);
|
|
8066
7866
|
});
|
|
7867
|
+
const migrations = configureCommand(admin.command("migrations").description("One-off operator data migrations"));
|
|
7868
|
+
migrations.action(() => {
|
|
7869
|
+
migrations.outputHelp();
|
|
7870
|
+
});
|
|
7871
|
+
migrations.command("preview-archive").description("Move archived 30s previews from the public bucket to the private one (REF-05)").option("--execute", "Actually perform the migration (default is a dry run)", false).option("--delete-public", "Delete phase: sweep the whole public analysis/previews/ prefix (run only after a full copy)", false).option("--verify", "Read-only: count the objects still under the public analysis/previews/ prefix", false).option("--limit <limit>", "Max findings/objects to process per pass", "50").option("--json", "Print JSON", false).action(async (options) => {
|
|
7872
|
+
const { migratePreviewArchiveCommand: migratePreviewArchiveCommand2 } = await Promise.resolve().then(() => (init_preview_migration(), exports_preview_migration));
|
|
7873
|
+
await runMigratePreviewArchive(options, migratePreviewArchiveCommand2);
|
|
7874
|
+
});
|
|
8067
7875
|
}
|
|
8068
7876
|
async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCommand2) {
|
|
8069
7877
|
if (!idOrLogId || !options.file || !options.source || !options.mime) {
|
|
@@ -8084,7 +7892,7 @@ async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCo
|
|
|
8084
7892
|
console.log(` mime: ${result.mime}`);
|
|
8085
7893
|
}
|
|
8086
7894
|
async function runTrackObserve(idOrLogId, options, trackObserveCommand2) {
|
|
8087
|
-
const script = options.scriptFile ?
|
|
7895
|
+
const script = options.scriptFile ? readFileSync5(options.scriptFile, "utf8") : options.script;
|
|
8088
7896
|
if (!idOrLogId || !script || !script.trim()) {
|
|
8089
7897
|
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]");
|
|
8090
7898
|
}
|
|
@@ -8140,7 +7948,7 @@ async function runTrackContext(idOrLogId, options, trackContextCommand2) {
|
|
|
8140
7948
|
}
|
|
8141
7949
|
}
|
|
8142
7950
|
async function runTrackNote(idOrLogId, options, trackNoteCommand2) {
|
|
8143
|
-
const note = options.scriptFile ?
|
|
7951
|
+
const note = options.scriptFile ? readFileSync5(options.scriptFile, "utf8") : options.script;
|
|
8144
7952
|
if (!idOrLogId || !note || !note.trim()) {
|
|
8145
7953
|
throw new Error("Usage: fluncle admin tracks note <track_id|log_id> (--script <text> | --script-file <file>) [--json]");
|
|
8146
7954
|
}
|
|
@@ -8156,6 +7964,88 @@ async function runTrackNote(idOrLogId, options, trackNoteCommand2) {
|
|
|
8156
7964
|
console.log(`Authored the note for ${result.logId}:`);
|
|
8157
7965
|
console.log(` ${result.note}`);
|
|
8158
7966
|
}
|
|
7967
|
+
async function runMigratePreviewArchive(options, migratePreviewArchiveCommand2) {
|
|
7968
|
+
const limit = options.limit === undefined ? 50 : Number.parseInt(options.limit, 10);
|
|
7969
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
7970
|
+
throw new Error("Limit must be a positive integer");
|
|
7971
|
+
}
|
|
7972
|
+
const mode = options.verify ? "verify" : options.deletePublic ? "delete" : "copy";
|
|
7973
|
+
const dryRun = mode === "verify" ? true : !options.execute;
|
|
7974
|
+
if (mode === "verify") {
|
|
7975
|
+
const result = await migratePreviewArchiveCommand2({ dryRun: true, limit, mode });
|
|
7976
|
+
if (options.json) {
|
|
7977
|
+
printJson({ mode, ok: true, remaining: result.remaining, sampleKeys: result.sampleKeys });
|
|
7978
|
+
return;
|
|
7979
|
+
}
|
|
7980
|
+
console.log(`${result.remaining} object(s) remain under the public analysis/previews/ prefix.`);
|
|
7981
|
+
for (const key of result.sampleKeys) {
|
|
7982
|
+
console.log(` ${key}`);
|
|
7983
|
+
}
|
|
7984
|
+
return;
|
|
7985
|
+
}
|
|
7986
|
+
const copied = [];
|
|
7987
|
+
const deleted = [];
|
|
7988
|
+
const skipped = [];
|
|
7989
|
+
const failed = [];
|
|
7990
|
+
let cursor;
|
|
7991
|
+
let remaining = 0;
|
|
7992
|
+
let blocked = null;
|
|
7993
|
+
for (;; ) {
|
|
7994
|
+
const result = await migratePreviewArchiveCommand2({ cursor, dryRun, limit, mode });
|
|
7995
|
+
if (result.blocked) {
|
|
7996
|
+
blocked = result.blocked;
|
|
7997
|
+
remaining = result.remaining;
|
|
7998
|
+
break;
|
|
7999
|
+
}
|
|
8000
|
+
copied.push(...result.copied);
|
|
8001
|
+
deleted.push(...result.deleted);
|
|
8002
|
+
skipped.push(...result.skipped);
|
|
8003
|
+
failed.push(...result.failed);
|
|
8004
|
+
remaining = result.remaining;
|
|
8005
|
+
if (!options.json) {
|
|
8006
|
+
const verb = dryRun ? "would" : "did";
|
|
8007
|
+
console.log(` \u2026${mode} pass (${verb}): +${result.copiedCount} copied, +${result.deletedCount} deleted, ${result.skippedCount} skipped, ${result.failedCount} failed; ${result.remaining} remaining`);
|
|
8008
|
+
}
|
|
8009
|
+
if (result.nextCursor === null) {
|
|
8010
|
+
break;
|
|
8011
|
+
}
|
|
8012
|
+
cursor = result.nextCursor;
|
|
8013
|
+
}
|
|
8014
|
+
if (options.json) {
|
|
8015
|
+
printJson({
|
|
8016
|
+
blocked,
|
|
8017
|
+
copied,
|
|
8018
|
+
copiedCount: copied.length,
|
|
8019
|
+
deleted,
|
|
8020
|
+
deletedCount: deleted.length,
|
|
8021
|
+
dryRun,
|
|
8022
|
+
failed,
|
|
8023
|
+
failedCount: failed.length,
|
|
8024
|
+
mode,
|
|
8025
|
+
ok: true,
|
|
8026
|
+
remaining,
|
|
8027
|
+
skipped,
|
|
8028
|
+
skippedCount: skipped.length
|
|
8029
|
+
});
|
|
8030
|
+
return;
|
|
8031
|
+
}
|
|
8032
|
+
if (blocked) {
|
|
8033
|
+
console.log(`REFUSED (${blocked}): ${remaining} legacy row(s) still need copying first.`);
|
|
8034
|
+
return;
|
|
8035
|
+
}
|
|
8036
|
+
const done = mode === "delete" ? deleted.length : copied.length;
|
|
8037
|
+
const noun = mode === "delete" ? "public object(s) deleted" : "preview(s) copied to the private bucket";
|
|
8038
|
+
const prefix = dryRun ? "DRY RUN \u2014 would have: " : "";
|
|
8039
|
+
console.log(`${prefix}${done} ${noun}; ${skipped.length} skipped; ${failed.length} failed; ${remaining} still under the prefix.`);
|
|
8040
|
+
for (const item of skipped) {
|
|
8041
|
+
if (item.reason === "private_copy_absent") {
|
|
8042
|
+
console.log(` LEFT (no private copy) ${item.trackId}`);
|
|
8043
|
+
}
|
|
8044
|
+
}
|
|
8045
|
+
for (const item of failed) {
|
|
8046
|
+
console.log(` FAILED ${item.trackId}: ${item.error}`);
|
|
8047
|
+
}
|
|
8048
|
+
}
|
|
8159
8049
|
async function runPreviewArchiveBackfill(options, previewArchiveBackfillCommand2) {
|
|
8160
8050
|
const limit = options.limit === undefined ? undefined : Number.parseInt(options.limit, 10);
|
|
8161
8051
|
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {
|
|
@@ -8329,7 +8219,7 @@ async function runArtistResolveQueue(options, listArtistsCommand2) {
|
|
|
8329
8219
|
return;
|
|
8330
8220
|
}
|
|
8331
8221
|
if (result.artists.length === 0) {
|
|
8332
|
-
console.log("
|
|
8222
|
+
console.log("Resolved every artist. Nothing waiting on the sweep.");
|
|
8333
8223
|
return;
|
|
8334
8224
|
}
|
|
8335
8225
|
const noun = result.artists.length === 1 ? "artist" : "artists";
|
|
@@ -8371,7 +8261,7 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
|
|
|
8371
8261
|
return;
|
|
8372
8262
|
}
|
|
8373
8263
|
const candidate = path2.join(dir, name);
|
|
8374
|
-
return
|
|
8264
|
+
return existsSync5(candidate) ? candidate : undefined;
|
|
8375
8265
|
};
|
|
8376
8266
|
const resolveFile = (explicit, name) => {
|
|
8377
8267
|
if (explicit) {
|
|
@@ -8555,7 +8445,7 @@ async function runTrackUpdate(trackId, options, trackUpdateCommand2) {
|
|
|
8555
8445
|
if (bpm !== undefined && !Number.isFinite(bpm)) {
|
|
8556
8446
|
throw new Error(`Invalid --bpm: ${options.bpm}`);
|
|
8557
8447
|
}
|
|
8558
|
-
const embeddingRaw = options.embeddingFile ?
|
|
8448
|
+
const embeddingRaw = options.embeddingFile ? readFileSync5(options.embeddingFile, "utf8") : options.embedding;
|
|
8559
8449
|
const embedding = parseEmbeddingArg(embeddingRaw);
|
|
8560
8450
|
const result = await trackUpdateCommand2(trackId, {
|
|
8561
8451
|
bpm,
|
|
@@ -8581,7 +8471,7 @@ async function runTrackRequeueVideo(idOrLogId, options, trackRequeueVideoCommand
|
|
|
8581
8471
|
printJson(result);
|
|
8582
8472
|
return;
|
|
8583
8473
|
}
|
|
8584
|
-
console.log(result.alreadyClear ? `${result.logId} already had no video
|
|
8474
|
+
console.log(result.alreadyClear ? `${result.logId} already had no video. Nothing to clear.` : `Cleared the video for ${result.logId}. It's back on the render queue (and off radio until re-rendered).`);
|
|
8585
8475
|
}
|
|
8586
8476
|
async function runTrackPurgeVideo(idOrLogId, options, trackPurgeVideoCommand2) {
|
|
8587
8477
|
if (!idOrLogId) {
|
|
@@ -8592,7 +8482,7 @@ async function runTrackPurgeVideo(idOrLogId, options, trackPurgeVideoCommand2) {
|
|
|
8592
8482
|
printJson(result);
|
|
8593
8483
|
return;
|
|
8594
8484
|
}
|
|
8595
|
-
console.log(result.noVideo ? `${result.logId} has no video
|
|
8485
|
+
console.log(result.noVideo ? `${result.logId} has no video. Nothing to purge.` : `Purging the stale renditions for ${result.logId} from the edge. The next play picks up the fresh render.`);
|
|
8596
8486
|
}
|
|
8597
8487
|
async function runMixtapeUpdate(id, options, mixtapeUpdateCommand2) {
|
|
8598
8488
|
if (!id) {
|
|
@@ -8780,14 +8670,14 @@ async function runNewsletterSend(id, options, newsletterSendCommand2) {
|
|
|
8780
8670
|
return;
|
|
8781
8671
|
}
|
|
8782
8672
|
const number = result.edition.number;
|
|
8783
|
-
console.log(number === undefined ? `Sent edition ${result.edition.id}.` : `Sent edition #${number}
|
|
8673
|
+
console.log(number === undefined ? `Sent edition ${result.edition.id}.` : `Sent edition #${number}. It's out to the list and in the archive.`);
|
|
8784
8674
|
}
|
|
8785
8675
|
async function runNewsletterDelete(id, options, newsletterDeleteCommand2) {
|
|
8786
8676
|
if (!id) {
|
|
8787
8677
|
throw new Error("Missing edition id. Usage: fluncle admin newsletter delete <id> --yes");
|
|
8788
8678
|
}
|
|
8789
8679
|
if (!options.yes) {
|
|
8790
|
-
throw new Error("Pass --yes to confirm
|
|
8680
|
+
throw new Error("Pass --yes to confirm. This hard-deletes the edition (draft or sent).");
|
|
8791
8681
|
}
|
|
8792
8682
|
const result = await newsletterDeleteCommand2(id);
|
|
8793
8683
|
if (options.json) {
|
package/package.json
CHANGED