fluncle 0.106.0 → 0.109.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 +114 -396
- 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.109.0".trim() ? "0.109.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.109.0".trim() ? "0.109.0".trim() : "0.1.0";
|
|
2211
2211
|
});
|
|
2212
2212
|
|
|
2213
2213
|
// ../../packages/registry/src/index.ts
|
|
@@ -2864,17 +2864,6 @@ var init_src = __esm(() => {
|
|
|
2864
2864
|
probeConfig: { cadenceMs: 60 * MINUTE_MS, cronName: "fluncle-artist-sweep", kind: "cron" },
|
|
2865
2865
|
weights: { status: "hidden" }
|
|
2866
2866
|
},
|
|
2867
|
-
{
|
|
2868
|
-
command: "fluncle admin artists follow",
|
|
2869
|
-
exposedContent: [
|
|
2870
|
-
"auto-follow high-confidence artists on Spotify + YouTube — the championing motion (--no-agent, Worker HTTP)"
|
|
2871
|
-
],
|
|
2872
|
-
kind: "cron",
|
|
2873
|
-
name: "cron.artist-follow",
|
|
2874
|
-
operatorNotes: "every 6h. Pure HTTP trigger, zero LLM tokens. Agent tier (the box holds no Spotify/YouTube tokens; the Worker does the PUT /me/following + subscriptions.insert and stamps followed_at). Idempotent by followed_at IS NULL, acting only on status IN (auto, confirmed); quota-paced. Mixcloud is link-only (cut). Source: docs/agents/hermes/scripts/artist-follow-sweep.*. Probed on /status as cron.artist-follow.",
|
|
2875
|
-
probeConfig: { cadenceMs: 6 * 60 * MINUTE_MS, cronName: "fluncle-artist-follow", kind: "cron" },
|
|
2876
|
-
weights: { status: "hidden" }
|
|
2877
|
-
},
|
|
2878
2867
|
{
|
|
2879
2868
|
command: "fluncle admin tracks social --capture",
|
|
2880
2869
|
exposedContent: [
|
|
@@ -2939,7 +2928,8 @@ var init_src = __esm(() => {
|
|
|
2939
2928
|
probeConfig: {
|
|
2940
2929
|
cadenceMs: 7 * 24 * 60 * MINUTE_MS,
|
|
2941
2930
|
cronName: "fluncle-newsletter",
|
|
2942
|
-
kind: "cron"
|
|
2931
|
+
kind: "cron",
|
|
2932
|
+
schedule: { time: "15:00", tz: "Europe/Amsterdam", weekday: 5 }
|
|
2943
2933
|
},
|
|
2944
2934
|
weights: { status: "secondary" }
|
|
2945
2935
|
},
|
|
@@ -2950,7 +2940,42 @@ var init_src = __esm(() => {
|
|
|
2950
2940
|
kind: "cron",
|
|
2951
2941
|
name: "cron.backup",
|
|
2952
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.*",
|
|
2953
|
-
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
|
+
},
|
|
2954
2979
|
weights: { status: "secondary" }
|
|
2955
2980
|
}
|
|
2956
2981
|
];
|
|
@@ -3063,7 +3088,7 @@ function checkBundleCompleteness(files) {
|
|
|
3063
3088
|
const missingFrom = (specs) => uploadingFootage ? specs.filter((spec) => !files[spec.option]).map((spec) => spec.file) : [];
|
|
3064
3089
|
const plateWarnings = [];
|
|
3065
3090
|
if (files.plateBackground && !files.plate) {
|
|
3066
|
-
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");
|
|
3067
3092
|
}
|
|
3068
3093
|
return {
|
|
3069
3094
|
missingAdvisory: missingFrom(RERENDER_ADVISORY_FIELDS),
|
|
@@ -3079,10 +3104,10 @@ async function trackVideoCommand(idOrLogId, files, onProgress, options = {}) {
|
|
|
3079
3104
|
}
|
|
3080
3105
|
if (completeness.uploadingFootage) {
|
|
3081
3106
|
if (completeness.missingContract.length > 0) {
|
|
3082
|
-
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`);
|
|
3083
3108
|
}
|
|
3084
3109
|
for (const missing of completeness.missingAdvisory) {
|
|
3085
|
-
onProgress?.(`warning: ${missing} missing (provenance/eval only)
|
|
3110
|
+
onProgress?.(`warning: ${missing} missing (provenance/eval only), shipping without it`);
|
|
3086
3111
|
}
|
|
3087
3112
|
}
|
|
3088
3113
|
for (const warning of completeness.plateWarnings) {
|
|
@@ -3117,7 +3142,7 @@ async function trackVideoCommand(idOrLogId, files, onProgress, options = {}) {
|
|
|
3117
3142
|
urls[spec.field] = `${FOUND_BASE}/${upload.key}`;
|
|
3118
3143
|
}
|
|
3119
3144
|
if (platesOnly) {
|
|
3120
|
-
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`);
|
|
3121
3146
|
return { logId: presign.logId, ok: true, trackId: presign.trackId, urls };
|
|
3122
3147
|
}
|
|
3123
3148
|
const manifest = files.render ? await readManifestFields(files.render) : {};
|
|
@@ -3275,246 +3300,6 @@ var init_track = __esm(() => {
|
|
|
3275
3300
|
NON_FILE_OPTIONS = ["model", "reasoning"];
|
|
3276
3301
|
});
|
|
3277
3302
|
|
|
3278
|
-
// src/commands/helm.ts
|
|
3279
|
-
var exports_helm = {};
|
|
3280
|
-
__export(exports_helm, {
|
|
3281
|
-
resolveHelmDir: () => resolveHelmDir,
|
|
3282
|
-
helmUninstallCommand: () => helmUninstallCommand,
|
|
3283
|
-
helmOpenCommand: () => helmOpenCommand,
|
|
3284
|
-
helmInstallCommand: () => helmInstallCommand,
|
|
3285
|
-
buildLaunchAgentPlist: () => buildLaunchAgentPlist
|
|
3286
|
-
});
|
|
3287
|
-
import { existsSync, mkdirSync as mkdirSync2, openSync, rmSync as rmSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
3288
|
-
import { homedir as homedir4 } from "node:os";
|
|
3289
|
-
import { dirname as dirname3, join as join4, resolve } from "node:path";
|
|
3290
|
-
function launchAgentPlistPath() {
|
|
3291
|
-
return join4(homedir4(), "Library/LaunchAgents", `${LAUNCH_AGENT_LABEL}.plist`);
|
|
3292
|
-
}
|
|
3293
|
-
function helmLogPath() {
|
|
3294
|
-
return join4(homedir4(), "Library/Logs/fluncle-helm.log");
|
|
3295
|
-
}
|
|
3296
|
-
function walkUpForHelm(start) {
|
|
3297
|
-
let dir = start;
|
|
3298
|
-
for (let depth = 0;depth < 8; depth++) {
|
|
3299
|
-
const candidate = join4(dir, "apps/helm");
|
|
3300
|
-
if (existsSync(join4(candidate, "src/server.ts"))) {
|
|
3301
|
-
return candidate;
|
|
3302
|
-
}
|
|
3303
|
-
const parent = dirname3(dir);
|
|
3304
|
-
if (parent === dir) {
|
|
3305
|
-
break;
|
|
3306
|
-
}
|
|
3307
|
-
dir = parent;
|
|
3308
|
-
}
|
|
3309
|
-
return null;
|
|
3310
|
-
}
|
|
3311
|
-
function persistHelmDir(dir) {
|
|
3312
|
-
try {
|
|
3313
|
-
mkdirSync2(dirname3(HELM_DIR_FILE), { recursive: true });
|
|
3314
|
-
writeFileSync2(HELM_DIR_FILE, `${dir}
|
|
3315
|
-
`, { mode: 384 });
|
|
3316
|
-
} catch {}
|
|
3317
|
-
}
|
|
3318
|
-
function resolveHelmDir() {
|
|
3319
|
-
const fromEnv = process.env.FLUNCLE_HELM_DIR;
|
|
3320
|
-
if (fromEnv) {
|
|
3321
|
-
if (!existsSync(join4(fromEnv, "src/server.ts"))) {
|
|
3322
|
-
throw new CliError2("helm_not_found", `FLUNCLE_HELM_DIR points at ${fromEnv}, but there's no helm there (src/server.ts missing).`);
|
|
3323
|
-
}
|
|
3324
|
-
return resolve(fromEnv);
|
|
3325
|
-
}
|
|
3326
|
-
const found = walkUpForHelm(process.cwd()) ?? walkUpForHelm(import.meta.dir);
|
|
3327
|
-
if (found) {
|
|
3328
|
-
persistHelmDir(found);
|
|
3329
|
-
return found;
|
|
3330
|
-
}
|
|
3331
|
-
try {
|
|
3332
|
-
const remembered = readFileSync2(HELM_DIR_FILE, "utf8").trim();
|
|
3333
|
-
if (remembered && existsSync(join4(remembered, "src/server.ts"))) {
|
|
3334
|
-
return remembered;
|
|
3335
|
-
}
|
|
3336
|
-
} catch {}
|
|
3337
|
-
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.");
|
|
3338
|
-
}
|
|
3339
|
-
function resolveBun() {
|
|
3340
|
-
const bun = Bun.which("bun");
|
|
3341
|
-
if (!bun) {
|
|
3342
|
-
throw new CliError2("no_bun", "No bun on the PATH. The helm daemon runs under bun.");
|
|
3343
|
-
}
|
|
3344
|
-
return bun;
|
|
3345
|
-
}
|
|
3346
|
-
async function daemonHealthy() {
|
|
3347
|
-
try {
|
|
3348
|
-
const response = await fetch(`${HELM_URL}/api/health`, {
|
|
3349
|
-
signal: AbortSignal.timeout(1500)
|
|
3350
|
-
});
|
|
3351
|
-
return response.ok;
|
|
3352
|
-
} catch {
|
|
3353
|
-
return false;
|
|
3354
|
-
}
|
|
3355
|
-
}
|
|
3356
|
-
async function ensureBuilt(helmDir) {
|
|
3357
|
-
if (existsSync(join4(helmDir, "dist/index.html"))) {
|
|
3358
|
-
return;
|
|
3359
|
-
}
|
|
3360
|
-
console.log("The glass isn't built yet. Building it once…");
|
|
3361
|
-
const proc = Bun.spawn([resolveBun(), "run", "build"], {
|
|
3362
|
-
cwd: helmDir,
|
|
3363
|
-
stderr: "inherit",
|
|
3364
|
-
stdin: "ignore",
|
|
3365
|
-
stdout: "inherit"
|
|
3366
|
-
});
|
|
3367
|
-
const exitCode = await proc.exited;
|
|
3368
|
-
if (exitCode !== 0) {
|
|
3369
|
-
throw new CliError2("helm_build_failed", `The glass build failed (exit ${exitCode}).`);
|
|
3370
|
-
}
|
|
3371
|
-
}
|
|
3372
|
-
function bootDaemon(helmDir) {
|
|
3373
|
-
const logPath = helmLogPath();
|
|
3374
|
-
mkdirSync2(dirname3(logPath), { recursive: true });
|
|
3375
|
-
const logFd = openSync(logPath, "a");
|
|
3376
|
-
const proc = Bun.spawn([resolveBun(), "src/server.ts"], {
|
|
3377
|
-
cwd: helmDir,
|
|
3378
|
-
stderr: logFd,
|
|
3379
|
-
stdin: "ignore",
|
|
3380
|
-
stdout: logFd
|
|
3381
|
-
});
|
|
3382
|
-
proc.unref();
|
|
3383
|
-
}
|
|
3384
|
-
async function waitForHealth(timeoutMs) {
|
|
3385
|
-
const deadline = Date.now() + timeoutMs;
|
|
3386
|
-
while (Date.now() < deadline) {
|
|
3387
|
-
if (await daemonHealthy()) {
|
|
3388
|
-
return true;
|
|
3389
|
-
}
|
|
3390
|
-
await Bun.sleep(250);
|
|
3391
|
-
}
|
|
3392
|
-
return false;
|
|
3393
|
-
}
|
|
3394
|
-
async function openAppWindow() {
|
|
3395
|
-
if (process.platform === "darwin" && chromeInstalled()) {
|
|
3396
|
-
const proc = Bun.spawn(["open", "-na", "Google Chrome", "--args", `--app=${HELM_URL}`], {
|
|
3397
|
-
stderr: "ignore",
|
|
3398
|
-
stdin: "ignore",
|
|
3399
|
-
stdout: "ignore"
|
|
3400
|
-
});
|
|
3401
|
-
if (await proc.exited === 0) {
|
|
3402
|
-
return;
|
|
3403
|
-
}
|
|
3404
|
-
}
|
|
3405
|
-
await openExternal(HELM_URL);
|
|
3406
|
-
}
|
|
3407
|
-
function chromeInstalled() {
|
|
3408
|
-
return existsSync("/Applications/Google Chrome.app") || existsSync(join4(homedir4(), "Applications/Google Chrome.app"));
|
|
3409
|
-
}
|
|
3410
|
-
async function helmOpenCommand() {
|
|
3411
|
-
if (await daemonHealthy()) {
|
|
3412
|
-
console.log(`The helm holds on :${HELM_PORT}. Opening the window.`);
|
|
3413
|
-
await openAppWindow();
|
|
3414
|
-
return;
|
|
3415
|
-
}
|
|
3416
|
-
const helmDir = resolveHelmDir();
|
|
3417
|
-
await ensureBuilt(helmDir);
|
|
3418
|
-
bootDaemon(helmDir);
|
|
3419
|
-
if (!await waitForHealth(15000)) {
|
|
3420
|
-
throw new CliError2("helm_no_answer", `The daemon didn't answer on :${HELM_PORT}. Its log is at ${helmLogPath()}.`);
|
|
3421
|
-
}
|
|
3422
|
-
console.log(`Helm raised on :${HELM_PORT}. Opening the window.`);
|
|
3423
|
-
await openAppWindow();
|
|
3424
|
-
}
|
|
3425
|
-
function xmlEscape(text) {
|
|
3426
|
-
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
3427
|
-
}
|
|
3428
|
-
function buildLaunchAgentPlist(helmDir, bunPath, logPath) {
|
|
3429
|
-
const path2 = `${dirname3(bunPath)}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`;
|
|
3430
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3431
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3432
|
-
<plist version="1.0">
|
|
3433
|
-
<dict>
|
|
3434
|
-
<key>EnvironmentVariables</key>
|
|
3435
|
-
<dict>
|
|
3436
|
-
<key>PATH</key>
|
|
3437
|
-
<string>${xmlEscape(path2)}</string>
|
|
3438
|
-
</dict>
|
|
3439
|
-
<key>KeepAlive</key>
|
|
3440
|
-
<dict>
|
|
3441
|
-
<key>SuccessfulExit</key>
|
|
3442
|
-
<false/>
|
|
3443
|
-
</dict>
|
|
3444
|
-
<key>Label</key>
|
|
3445
|
-
<string>${LAUNCH_AGENT_LABEL}</string>
|
|
3446
|
-
<key>ProgramArguments</key>
|
|
3447
|
-
<array>
|
|
3448
|
-
<string>${xmlEscape(bunPath)}</string>
|
|
3449
|
-
<string>src/server.ts</string>
|
|
3450
|
-
</array>
|
|
3451
|
-
<key>RunAtLoad</key>
|
|
3452
|
-
<true/>
|
|
3453
|
-
<key>StandardErrorPath</key>
|
|
3454
|
-
<string>${xmlEscape(logPath)}</string>
|
|
3455
|
-
<key>StandardOutPath</key>
|
|
3456
|
-
<string>${xmlEscape(logPath)}</string>
|
|
3457
|
-
<key>WorkingDirectory</key>
|
|
3458
|
-
<string>${xmlEscape(helmDir)}</string>
|
|
3459
|
-
</dict>
|
|
3460
|
-
</plist>
|
|
3461
|
-
`;
|
|
3462
|
-
}
|
|
3463
|
-
function currentUid() {
|
|
3464
|
-
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
3465
|
-
if (uid === undefined) {
|
|
3466
|
-
throw new CliError2("no_uid", "Couldn't read the user id — launchd needs a gui domain.");
|
|
3467
|
-
}
|
|
3468
|
-
return uid;
|
|
3469
|
-
}
|
|
3470
|
-
async function launchctl(args) {
|
|
3471
|
-
const proc = Bun.spawn(["launchctl", ...args], {
|
|
3472
|
-
stderr: "ignore",
|
|
3473
|
-
stdin: "ignore",
|
|
3474
|
-
stdout: "ignore"
|
|
3475
|
-
});
|
|
3476
|
-
return proc.exited;
|
|
3477
|
-
}
|
|
3478
|
-
async function helmInstallCommand() {
|
|
3479
|
-
if (process.platform !== "darwin") {
|
|
3480
|
-
throw new CliError2("unsupported_platform", "launchd is a macOS thing. Nothing to install here.");
|
|
3481
|
-
}
|
|
3482
|
-
const helmDir = resolveHelmDir();
|
|
3483
|
-
await ensureBuilt(helmDir);
|
|
3484
|
-
const plistPath = launchAgentPlistPath();
|
|
3485
|
-
mkdirSync2(dirname3(plistPath), { recursive: true });
|
|
3486
|
-
mkdirSync2(dirname3(helmLogPath()), { recursive: true });
|
|
3487
|
-
writeFileSync2(plistPath, buildLaunchAgentPlist(helmDir, resolveBun(), helmLogPath()));
|
|
3488
|
-
const uid = currentUid();
|
|
3489
|
-
await launchctl(["bootout", `gui/${uid}/${LAUNCH_AGENT_LABEL}`]);
|
|
3490
|
-
const exitCode = await launchctl(["bootstrap", `gui/${uid}`, plistPath]);
|
|
3491
|
-
if (exitCode !== 0) {
|
|
3492
|
-
throw new CliError2("launchctl_refused", `launchctl bootstrap refused (exit ${exitCode}). The plist is at ${plistPath}.`);
|
|
3493
|
-
}
|
|
3494
|
-
console.log(`LaunchAgent installed (${LAUNCH_AGENT_LABEL}).`);
|
|
3495
|
-
console.log(`The daemon rises at login and holds :${HELM_PORT}. Log: ${helmLogPath()}`);
|
|
3496
|
-
}
|
|
3497
|
-
async function helmUninstallCommand() {
|
|
3498
|
-
if (process.platform !== "darwin") {
|
|
3499
|
-
throw new CliError2("unsupported_platform", "launchd is a macOS thing. Nothing to remove here.");
|
|
3500
|
-
}
|
|
3501
|
-
const plistPath = launchAgentPlistPath();
|
|
3502
|
-
await launchctl(["bootout", `gui/${currentUid()}/${LAUNCH_AGENT_LABEL}`]);
|
|
3503
|
-
if (existsSync(plistPath)) {
|
|
3504
|
-
rmSync2(plistPath);
|
|
3505
|
-
console.log("LaunchAgent removed and the daemon stood down.");
|
|
3506
|
-
return;
|
|
3507
|
-
}
|
|
3508
|
-
console.log("No LaunchAgent on file. Nothing stood down.");
|
|
3509
|
-
}
|
|
3510
|
-
var HELM_PORT = 4190, HELM_URL, LAUNCH_AGENT_LABEL = "com.fluncle.helm", HELM_DIR_FILE;
|
|
3511
|
-
var init_helm = __esm(() => {
|
|
3512
|
-
init_open_external();
|
|
3513
|
-
init_output();
|
|
3514
|
-
HELM_URL = `http://127.0.0.1:${HELM_PORT}`;
|
|
3515
|
-
HELM_DIR_FILE = join4(homedir4(), ".config/fluncle/helm-dir");
|
|
3516
|
-
});
|
|
3517
|
-
|
|
3518
3303
|
// src/commands/add.ts
|
|
3519
3304
|
var exports_add = {};
|
|
3520
3305
|
__export(exports_add, {
|
|
@@ -3936,12 +3721,12 @@ async function distributeYoutube2(mixtapeId, videoPath, onProgress) {
|
|
|
3936
3721
|
continue;
|
|
3937
3722
|
}
|
|
3938
3723
|
if (result.kind === "reauth") {
|
|
3939
|
-
onProgress?.("Access token expired
|
|
3724
|
+
onProgress?.("Access token expired, re-minting and resuming");
|
|
3940
3725
|
session = { accessToken: await mintToken2(), sessionUri: session.sessionUri };
|
|
3941
3726
|
offset = await queryOffset2(session.sessionUri, session.accessToken, contentLength);
|
|
3942
3727
|
continue;
|
|
3943
3728
|
}
|
|
3944
|
-
onProgress?.("Upload session expired
|
|
3729
|
+
onProgress?.("Upload session expired, re-initiating");
|
|
3945
3730
|
session = await initiate2(mixtapeId, contentLength, contentType);
|
|
3946
3731
|
offset = 0;
|
|
3947
3732
|
}
|
|
@@ -4096,9 +3881,9 @@ var init_multipart = __esm(() => {
|
|
|
4096
3881
|
|
|
4097
3882
|
// src/commands/mixtape-set-video.ts
|
|
4098
3883
|
import { randomUUID } from "node:crypto";
|
|
4099
|
-
import { existsSync
|
|
3884
|
+
import { existsSync, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
|
|
4100
3885
|
import { tmpdir } from "node:os";
|
|
4101
|
-
import { join as
|
|
3886
|
+
import { join as join4 } from "node:path";
|
|
4102
3887
|
function renditionFfmpegArgs(inputPath, outputPath) {
|
|
4103
3888
|
return [
|
|
4104
3889
|
"-y",
|
|
@@ -4126,11 +3911,11 @@ function renditionFfmpegArgs(inputPath, outputPath) {
|
|
|
4126
3911
|
];
|
|
4127
3912
|
}
|
|
4128
3913
|
async function uploadRenditionMultipart(masterPath, presignPath, onProgress = () => {}) {
|
|
4129
|
-
if (!
|
|
3914
|
+
if (!existsSync(masterPath)) {
|
|
4130
3915
|
throw new CliError2("file_not_found", `Set-video master not found: ${masterPath}`);
|
|
4131
3916
|
}
|
|
4132
3917
|
await assertFfmpeg();
|
|
4133
|
-
const renditionPath =
|
|
3918
|
+
const renditionPath = join4(tmpdir(), `fluncle-set-${randomUUID()}.mp4`);
|
|
4134
3919
|
onProgress("Set video: deriving the 1080p faststart rendition (ffmpeg)…");
|
|
4135
3920
|
await deriveRendition(masterPath, renditionPath);
|
|
4136
3921
|
try {
|
|
@@ -4159,7 +3944,7 @@ async function uploadRenditionMultipart(masterPath, presignPath, onProgress = ()
|
|
|
4159
3944
|
}
|
|
4160
3945
|
return { key: presign.key, url: `${FOUND_BASE2}/${presign.key}` };
|
|
4161
3946
|
} finally {
|
|
4162
|
-
|
|
3947
|
+
rmSync2(renditionPath, { force: true });
|
|
4163
3948
|
}
|
|
4164
3949
|
}
|
|
4165
3950
|
async function putPart(url, path2, part, onProgress = () => {}) {
|
|
@@ -4175,8 +3960,8 @@ async function putPart(url, path2, part, onProgress = () => {}) {
|
|
|
4175
3960
|
throw new CliError2("r2_part_failed", `Part ${part.partNumber} failed after ${MAX_PART_ATTEMPTS} attempts: ${error instanceof Error ? error.message : String(error)}`);
|
|
4176
3961
|
}
|
|
4177
3962
|
const backoffMs = 500 * 2 ** (attempt - 1);
|
|
4178
|
-
onProgress(`Set video: part ${part.partNumber} dropped
|
|
4179
|
-
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));
|
|
4180
3965
|
}
|
|
4181
3966
|
}
|
|
4182
3967
|
}
|
|
@@ -4256,7 +4041,7 @@ __export(exports_recordings, {
|
|
|
4256
4041
|
recordingDeleteCommand: () => recordingDeleteCommand,
|
|
4257
4042
|
recordingCreateCommand: () => recordingCreateCommand
|
|
4258
4043
|
});
|
|
4259
|
-
import { existsSync as
|
|
4044
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
4260
4045
|
async function recordingGet(id) {
|
|
4261
4046
|
const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
4262
4047
|
return response.recording;
|
|
@@ -4315,7 +4100,7 @@ async function recordingCreateCommand(options = {}) {
|
|
|
4315
4100
|
printJson2({ ok: true, recording: recording2 });
|
|
4316
4101
|
return;
|
|
4317
4102
|
}
|
|
4318
|
-
console.log(`Plan ${recording2.id} created
|
|
4103
|
+
console.log(`Plan ${recording2.id} created, handle "${recording2.title}"`);
|
|
4319
4104
|
return;
|
|
4320
4105
|
}
|
|
4321
4106
|
const title = options.title?.trim();
|
|
@@ -4325,7 +4110,7 @@ async function recordingCreateCommand(options = {}) {
|
|
|
4325
4110
|
if (!options.video) {
|
|
4326
4111
|
throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
|
|
4327
4112
|
}
|
|
4328
|
-
if (!
|
|
4113
|
+
if (!existsSync2(options.video)) {
|
|
4329
4114
|
throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
|
|
4330
4115
|
}
|
|
4331
4116
|
const created = await adminApiPost("/api/admin/recordings", {
|
|
@@ -4338,7 +4123,7 @@ async function recordingCreateCommand(options = {}) {
|
|
|
4338
4123
|
console.log(message);
|
|
4339
4124
|
}
|
|
4340
4125
|
};
|
|
4341
|
-
log(`Recording ${recording.id} created
|
|
4126
|
+
log(`Recording ${recording.id} created, staging the set video…`);
|
|
4342
4127
|
const upload = await uploadRenditionMultipart(options.video, `/api/admin/recordings/${encodeURIComponent(recording.id)}/set-video/presign`, log);
|
|
4343
4128
|
if (options.json) {
|
|
4344
4129
|
printJson2({ key: upload.key, ok: true, recording, url: upload.url });
|
|
@@ -4361,17 +4146,17 @@ async function recordingUpdateCommand(id, options = {}) {
|
|
|
4361
4146
|
body.parentId = options.parentId;
|
|
4362
4147
|
}
|
|
4363
4148
|
if (options.tracklistFile !== undefined) {
|
|
4364
|
-
if (!
|
|
4149
|
+
if (!existsSync2(options.tracklistFile)) {
|
|
4365
4150
|
throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
|
|
4366
4151
|
}
|
|
4367
4152
|
try {
|
|
4368
|
-
body.tracklistJson = JSON.parse(
|
|
4153
|
+
body.tracklistJson = JSON.parse(readFileSync2(options.tracklistFile, "utf8"));
|
|
4369
4154
|
} catch {
|
|
4370
4155
|
throw new CliError2("invalid_tracklist", `Tracklist file is not valid JSON: ${options.tracklistFile}`);
|
|
4371
4156
|
}
|
|
4372
4157
|
}
|
|
4373
4158
|
if (Object.keys(body).length === 0) {
|
|
4374
|
-
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");
|
|
4375
4160
|
}
|
|
4376
4161
|
const response = await adminApiPatch(`/api/admin/recordings/${encodeURIComponent(id)}`, body);
|
|
4377
4162
|
if (options.json) {
|
|
@@ -4410,12 +4195,12 @@ async function recordingReplaceCuesCommand(id, options = {}) {
|
|
|
4410
4195
|
if (!options.cuesFile) {
|
|
4411
4196
|
throw new CliError2("missing_cues_file", "replace-cues needs a --cues-file <file> (the ordered cue array)");
|
|
4412
4197
|
}
|
|
4413
|
-
if (!
|
|
4198
|
+
if (!existsSync2(options.cuesFile)) {
|
|
4414
4199
|
throw new CliError2("file_not_found", `Cues file not found: ${options.cuesFile}`);
|
|
4415
4200
|
}
|
|
4416
4201
|
let cues;
|
|
4417
4202
|
try {
|
|
4418
|
-
cues = JSON.parse(
|
|
4203
|
+
cues = JSON.parse(readFileSync2(options.cuesFile, "utf8"));
|
|
4419
4204
|
} catch {
|
|
4420
4205
|
throw new CliError2("invalid_cues", `Cues file is not valid JSON: ${options.cuesFile}`);
|
|
4421
4206
|
}
|
|
@@ -4453,9 +4238,9 @@ __export(exports_clips, {
|
|
|
4453
4238
|
CLIP_AUDIO_BITRATE: () => CLIP_AUDIO_BITRATE
|
|
4454
4239
|
});
|
|
4455
4240
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4456
|
-
import { rmSync as
|
|
4241
|
+
import { rmSync as rmSync3, statSync as statSync4 } from "node:fs";
|
|
4457
4242
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
4458
|
-
import { join as
|
|
4243
|
+
import { join as join5 } from "node:path";
|
|
4459
4244
|
function clipFootageKey(clipId) {
|
|
4460
4245
|
return `${clipId}/footage.mp4`;
|
|
4461
4246
|
}
|
|
@@ -4552,7 +4337,7 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4552
4337
|
}
|
|
4553
4338
|
const source = await resolveClipSource(clip);
|
|
4554
4339
|
await assertFfmpeg2();
|
|
4555
|
-
const outputPath =
|
|
4340
|
+
const outputPath = join5(tmpdir2(), `fluncle-clip-${randomUUID2()}.mp4`);
|
|
4556
4341
|
try {
|
|
4557
4342
|
onProgress(`Clip ${clipId}: cutting [${clip.inMs}–${clip.outMs}ms]…`);
|
|
4558
4343
|
await runClipCut({
|
|
@@ -4564,7 +4349,7 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4564
4349
|
});
|
|
4565
4350
|
const sizeBytes = statSync4(outputPath).size;
|
|
4566
4351
|
if (sizeBytes > MAX_CLIP_BYTES) {
|
|
4567
|
-
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`);
|
|
4568
4353
|
}
|
|
4569
4354
|
onProgress(`Clip ${clipId}: uploading ${(sizeBytes / 1e6).toFixed(1)} MB…`);
|
|
4570
4355
|
const presign = await adminApiPost(`/api/admin/clips/${encodeURIComponent(clipId)}/cut/presign`, { contentType: "video/mp4" });
|
|
@@ -4578,7 +4363,7 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4578
4363
|
url: `${FOUND_BASE3}/${presign.key}`
|
|
4579
4364
|
};
|
|
4580
4365
|
} finally {
|
|
4581
|
-
|
|
4366
|
+
rmSync3(outputPath, { force: true });
|
|
4582
4367
|
}
|
|
4583
4368
|
}
|
|
4584
4369
|
async function putClip(url, contentType, path2) {
|
|
@@ -4636,7 +4421,7 @@ __export(exports_recordings2, {
|
|
|
4636
4421
|
recordingDeleteCommand: () => recordingDeleteCommand2,
|
|
4637
4422
|
recordingCreateCommand: () => recordingCreateCommand2
|
|
4638
4423
|
});
|
|
4639
|
-
import { existsSync as
|
|
4424
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
4640
4425
|
async function recordingGet2(id) {
|
|
4641
4426
|
const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
4642
4427
|
return response.recording;
|
|
@@ -4695,7 +4480,7 @@ async function recordingCreateCommand2(options = {}) {
|
|
|
4695
4480
|
printJson2({ ok: true, recording: recording2 });
|
|
4696
4481
|
return;
|
|
4697
4482
|
}
|
|
4698
|
-
console.log(`Plan ${recording2.id} created
|
|
4483
|
+
console.log(`Plan ${recording2.id} created, handle "${recording2.title}"`);
|
|
4699
4484
|
return;
|
|
4700
4485
|
}
|
|
4701
4486
|
const title = options.title?.trim();
|
|
@@ -4705,7 +4490,7 @@ async function recordingCreateCommand2(options = {}) {
|
|
|
4705
4490
|
if (!options.video) {
|
|
4706
4491
|
throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
|
|
4707
4492
|
}
|
|
4708
|
-
if (!
|
|
4493
|
+
if (!existsSync3(options.video)) {
|
|
4709
4494
|
throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
|
|
4710
4495
|
}
|
|
4711
4496
|
const created = await adminApiPost("/api/admin/recordings", {
|
|
@@ -4718,7 +4503,7 @@ async function recordingCreateCommand2(options = {}) {
|
|
|
4718
4503
|
console.log(message);
|
|
4719
4504
|
}
|
|
4720
4505
|
};
|
|
4721
|
-
log(`Recording ${recording.id} created
|
|
4506
|
+
log(`Recording ${recording.id} created, staging the set video…`);
|
|
4722
4507
|
const upload = await uploadRenditionMultipart(options.video, `/api/admin/recordings/${encodeURIComponent(recording.id)}/set-video/presign`, log);
|
|
4723
4508
|
if (options.json) {
|
|
4724
4509
|
printJson2({ key: upload.key, ok: true, recording, url: upload.url });
|
|
@@ -4741,17 +4526,17 @@ async function recordingUpdateCommand2(id, options = {}) {
|
|
|
4741
4526
|
body.parentId = options.parentId;
|
|
4742
4527
|
}
|
|
4743
4528
|
if (options.tracklistFile !== undefined) {
|
|
4744
|
-
if (!
|
|
4529
|
+
if (!existsSync3(options.tracklistFile)) {
|
|
4745
4530
|
throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
|
|
4746
4531
|
}
|
|
4747
4532
|
try {
|
|
4748
|
-
body.tracklistJson = JSON.parse(
|
|
4533
|
+
body.tracklistJson = JSON.parse(readFileSync3(options.tracklistFile, "utf8"));
|
|
4749
4534
|
} catch {
|
|
4750
4535
|
throw new CliError2("invalid_tracklist", `Tracklist file is not valid JSON: ${options.tracklistFile}`);
|
|
4751
4536
|
}
|
|
4752
4537
|
}
|
|
4753
4538
|
if (Object.keys(body).length === 0) {
|
|
4754
|
-
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");
|
|
4755
4540
|
}
|
|
4756
4541
|
const response = await adminApiPatch(`/api/admin/recordings/${encodeURIComponent(id)}`, body);
|
|
4757
4542
|
if (options.json) {
|
|
@@ -4790,12 +4575,12 @@ async function recordingReplaceCuesCommand2(id, options = {}) {
|
|
|
4790
4575
|
if (!options.cuesFile) {
|
|
4791
4576
|
throw new CliError2("missing_cues_file", "replace-cues needs a --cues-file <file> (the ordered cue array)");
|
|
4792
4577
|
}
|
|
4793
|
-
if (!
|
|
4578
|
+
if (!existsSync3(options.cuesFile)) {
|
|
4794
4579
|
throw new CliError2("file_not_found", `Cues file not found: ${options.cuesFile}`);
|
|
4795
4580
|
}
|
|
4796
4581
|
let cues;
|
|
4797
4582
|
try {
|
|
4798
|
-
cues = JSON.parse(
|
|
4583
|
+
cues = JSON.parse(readFileSync3(options.cuesFile, "utf8"));
|
|
4799
4584
|
} catch {
|
|
4800
4585
|
throw new CliError2("invalid_cues", `Cues file is not valid JSON: ${options.cuesFile}`);
|
|
4801
4586
|
}
|
|
@@ -4821,7 +4606,7 @@ __export(exports_newsletter, {
|
|
|
4821
4606
|
newsletterDraftCommand: () => newsletterDraftCommand,
|
|
4822
4607
|
newsletterDeleteCommand: () => newsletterDeleteCommand
|
|
4823
4608
|
});
|
|
4824
|
-
import { existsSync as
|
|
4609
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
4825
4610
|
function buildBody2(options, { requireContent }) {
|
|
4826
4611
|
const body = {};
|
|
4827
4612
|
if (options.contentFile !== undefined) {
|
|
@@ -4841,10 +4626,10 @@ function buildBody2(options, { requireContent }) {
|
|
|
4841
4626
|
return body;
|
|
4842
4627
|
}
|
|
4843
4628
|
function readContentFile(filePath) {
|
|
4844
|
-
if (!
|
|
4629
|
+
if (!existsSync4(filePath)) {
|
|
4845
4630
|
throw new CliError2("file_not_found", `Content file not found: ${filePath}`);
|
|
4846
4631
|
}
|
|
4847
|
-
const text =
|
|
4632
|
+
const text = readFileSync4(filePath, "utf-8");
|
|
4848
4633
|
try {
|
|
4849
4634
|
return JSON.parse(text);
|
|
4850
4635
|
} catch (error) {
|
|
@@ -5205,7 +4990,6 @@ var exports_admin_artists = {};
|
|
|
5205
4990
|
__export(exports_admin_artists, {
|
|
5206
4991
|
resolveArtistCommand: () => resolveArtistCommand,
|
|
5207
4992
|
listArtistsCommand: () => listArtistsCommand,
|
|
5208
|
-
followArtistsCommand: () => followArtistsCommand,
|
|
5209
4993
|
backfillArtistsCommand: () => backfillArtistsCommand
|
|
5210
4994
|
});
|
|
5211
4995
|
async function listArtistsCommand(limit, cursor) {
|
|
@@ -5225,10 +5009,6 @@ async function backfillArtistsCommand(limit, dryRun, cursor) {
|
|
|
5225
5009
|
}
|
|
5226
5010
|
return adminApiPost(`/api/admin/backfill/artists?${params.toString()}`);
|
|
5227
5011
|
}
|
|
5228
|
-
async function followArtistsCommand(limit, dryRun) {
|
|
5229
|
-
const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
|
|
5230
|
-
return adminApiPost(`/api/admin/artists/follow?${params.toString()}`);
|
|
5231
|
-
}
|
|
5232
5012
|
var init_admin_artists = __esm(() => {
|
|
5233
5013
|
init_api();
|
|
5234
5014
|
});
|
|
@@ -5266,7 +5046,7 @@ async function selectWithKeyboard2(items, options) {
|
|
|
5266
5046
|
let renderedLines = 0;
|
|
5267
5047
|
let done = false;
|
|
5268
5048
|
const wasRaw = stdin.isRaw === true;
|
|
5269
|
-
return await new Promise((
|
|
5049
|
+
return await new Promise((resolve) => {
|
|
5270
5050
|
function cleanup() {
|
|
5271
5051
|
if (done) {
|
|
5272
5052
|
return;
|
|
@@ -5281,14 +5061,14 @@ async function selectWithKeyboard2(items, options) {
|
|
|
5281
5061
|
cleanup();
|
|
5282
5062
|
stdout.write(renderedLines > 0 ? `
|
|
5283
5063
|
` : "");
|
|
5284
|
-
|
|
5064
|
+
resolve(item);
|
|
5285
5065
|
}
|
|
5286
5066
|
function cancel() {
|
|
5287
5067
|
cleanup();
|
|
5288
5068
|
clearRendered2(stdout, renderedLines);
|
|
5289
5069
|
stdout.write(`Cancelled.
|
|
5290
5070
|
`);
|
|
5291
|
-
|
|
5071
|
+
resolve(undefined);
|
|
5292
5072
|
}
|
|
5293
5073
|
function render() {
|
|
5294
5074
|
clearRendered2(stdout, renderedLines);
|
|
@@ -5344,7 +5124,7 @@ async function paginateWithKeyboard(options) {
|
|
|
5344
5124
|
let busy = false;
|
|
5345
5125
|
let done = false;
|
|
5346
5126
|
const wasRaw = stdin.isRaw === true;
|
|
5347
|
-
return await new Promise((
|
|
5127
|
+
return await new Promise((resolve) => {
|
|
5348
5128
|
function cleanup() {
|
|
5349
5129
|
if (done) {
|
|
5350
5130
|
return;
|
|
@@ -5359,7 +5139,7 @@ async function paginateWithKeyboard(options) {
|
|
|
5359
5139
|
cleanup();
|
|
5360
5140
|
stdout.write(`
|
|
5361
5141
|
`);
|
|
5362
|
-
|
|
5142
|
+
resolve();
|
|
5363
5143
|
}
|
|
5364
5144
|
function render() {
|
|
5365
5145
|
clearRendered2(stdout, renderedLines);
|
|
@@ -5512,7 +5292,7 @@ var init_format2 = __esm(() => {
|
|
|
5512
5292
|
});
|
|
5513
5293
|
|
|
5514
5294
|
// src/cli.ts
|
|
5515
|
-
import { existsSync as
|
|
5295
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
5516
5296
|
import path2 from "path";
|
|
5517
5297
|
|
|
5518
5298
|
// ../../node_modules/commander/lib/error.js
|
|
@@ -7658,7 +7438,6 @@ ${fluncleAsciiLogo}
|
|
|
7658
7438
|
addMetaCommands(program2);
|
|
7659
7439
|
addTrackCommands(program2);
|
|
7660
7440
|
addAdminCommands(program2);
|
|
7661
|
-
addHelmCommands(program2);
|
|
7662
7441
|
return program2;
|
|
7663
7442
|
}
|
|
7664
7443
|
async function main(args = process.argv.slice(2)) {
|
|
@@ -7696,7 +7475,7 @@ function addListenCommands(program2) {
|
|
|
7696
7475
|
const { recentCommand: recentCommand3 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
|
|
7697
7476
|
await runRecent(options, recentCommand3);
|
|
7698
7477
|
});
|
|
7699
|
-
program2.command("artists").description("Browse artists in Fluncle's archive").argument("[slug]", "Artist slug
|
|
7478
|
+
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) => {
|
|
7700
7479
|
const { artistsListCommand: artistsListCommand2, artistsGetCommand: artistsGetCommand2 } = await Promise.resolve().then(() => (init_artists(), exports_artists));
|
|
7701
7480
|
await runArtists(slug, options, { artistsGetCommand: artistsGetCommand2, artistsListCommand: artistsListCommand2 });
|
|
7702
7481
|
});
|
|
@@ -7761,21 +7540,6 @@ function addTrackCommands(program2) {
|
|
|
7761
7540
|
await runTrackGet(idOrLogId, options, trackGetCommand2);
|
|
7762
7541
|
});
|
|
7763
7542
|
}
|
|
7764
|
-
function addHelmCommands(program2) {
|
|
7765
|
-
const helm = configureCommand(program2.command("helm", { hidden: true }).description("Fluncle's Helm \u2014 the operator's mission control"));
|
|
7766
|
-
helm.action(async () => {
|
|
7767
|
-
const { helmOpenCommand: helmOpenCommand2 } = await Promise.resolve().then(() => (init_helm(), exports_helm));
|
|
7768
|
-
await helmOpenCommand2();
|
|
7769
|
-
});
|
|
7770
|
-
helm.command("install").description("Install the launchd LaunchAgent (the daemon rises at login)").action(async () => {
|
|
7771
|
-
const { helmInstallCommand: helmInstallCommand2 } = await Promise.resolve().then(() => (init_helm(), exports_helm));
|
|
7772
|
-
await helmInstallCommand2();
|
|
7773
|
-
});
|
|
7774
|
-
helm.command("uninstall").description("Remove the LaunchAgent and stand the daemon down").action(async () => {
|
|
7775
|
-
const { helmUninstallCommand: helmUninstallCommand2 } = await Promise.resolve().then(() => (init_helm(), exports_helm));
|
|
7776
|
-
await helmUninstallCommand2();
|
|
7777
|
-
});
|
|
7778
|
-
}
|
|
7779
7543
|
function addAdminCommands(program2) {
|
|
7780
7544
|
const admin = configureCommand(program2.command("admin", { hidden: true }).description("Operator commands"));
|
|
7781
7545
|
admin.action(() => {
|
|
@@ -7800,27 +7564,27 @@ function addAdminCommands(program2) {
|
|
|
7800
7564
|
const { listCommand: listCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
7801
7565
|
await runAdminTracksList(options, listCommand2);
|
|
7802
7566
|
});
|
|
7803
|
-
adminTracks.command("enrich").description("Enrichment worklist (pending, failed, or stuck processing)
|
|
7567
|
+
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) => {
|
|
7804
7568
|
if (!options.queue) {
|
|
7805
|
-
console.error("`tracks enrich` is a worklist view
|
|
7569
|
+
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.");
|
|
7806
7570
|
process.exitCode = 1;
|
|
7807
7571
|
return;
|
|
7808
7572
|
}
|
|
7809
7573
|
const { enrichQueueCommand: enrichQueueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
7810
7574
|
await runAdminEnrichQueue(options, enrichQueueCommand2);
|
|
7811
7575
|
});
|
|
7812
|
-
adminTracks.command("embed").description("Audio-embedding worklist (findings with no MuQ vector yet)
|
|
7576
|
+
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) => {
|
|
7813
7577
|
if (!options.queue) {
|
|
7814
|
-
console.error("`tracks embed` is a worklist view
|
|
7578
|
+
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.");
|
|
7815
7579
|
process.exitCode = 1;
|
|
7816
7580
|
return;
|
|
7817
7581
|
}
|
|
7818
7582
|
const { embedQueueCommand: embedQueueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
7819
7583
|
await runAdminEmbedQueue(options, embedQueueCommand2);
|
|
7820
7584
|
});
|
|
7821
|
-
adminTracks.command("capture-audio").description("Full-song capture worklist (findings with no source audio yet)
|
|
7585
|
+
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) => {
|
|
7822
7586
|
if (!options.queue) {
|
|
7823
|
-
console.error("`tracks capture-audio` is a worklist view
|
|
7587
|
+
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.");
|
|
7824
7588
|
process.exitCode = 1;
|
|
7825
7589
|
return;
|
|
7826
7590
|
}
|
|
@@ -7912,7 +7676,7 @@ function addAdminCommands(program2) {
|
|
|
7912
7676
|
const { mixtapeGetCommand: mixtapeGetCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7913
7677
|
await runMixtapeGet(idOrLogId, options, mixtapeGetCommand2);
|
|
7914
7678
|
});
|
|
7915
|
-
adminMixtapes.command("distribute").description("Push a promoted mixtape to YouTube (video) and Mixcloud (audio). The mixtape must already be promoted (`recordings promote`)
|
|
7679
|
+
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) => {
|
|
7916
7680
|
const { mixtapeDistributeCommand: mixtapeDistributeCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7917
7681
|
await runMixtapeDistribute(idOrLogId, options, mixtapeDistributeCommand2);
|
|
7918
7682
|
});
|
|
@@ -7936,11 +7700,11 @@ function addAdminCommands(program2) {
|
|
|
7936
7700
|
const { clipScheduleCommand: clipScheduleCommand2 } = await Promise.resolve().then(() => (init_clips(), exports_clips));
|
|
7937
7701
|
await runClipsSchedule(clipId, options, clipScheduleCommand2);
|
|
7938
7702
|
});
|
|
7939
|
-
adminClips.command("drip-pause").description("Pause the whole Instagram drip-feed
|
|
7703
|
+
adminClips.command("drip-pause").description("Pause the whole Instagram drip-feed, the kill switch (operator)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
7940
7704
|
const { clipDripPauseCommand: clipDripPauseCommand2 } = await Promise.resolve().then(() => (init_clips(), exports_clips));
|
|
7941
7705
|
await runClipsDripPause(true, options, clipDripPauseCommand2);
|
|
7942
7706
|
});
|
|
7943
|
-
adminClips.command("drip-resume").description("Resume the Instagram drip-feed
|
|
7707
|
+
adminClips.command("drip-resume").description("Resume the Instagram drip-feed, clear the kill switch (operator)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
7944
7708
|
const { clipDripPauseCommand: clipDripPauseCommand2 } = await Promise.resolve().then(() => (init_clips(), exports_clips));
|
|
7945
7709
|
await runClipsDripPause(false, options, clipDripPauseCommand2);
|
|
7946
7710
|
});
|
|
@@ -7992,7 +7756,7 @@ function addAdminCommands(program2) {
|
|
|
7992
7756
|
const { newsletterUpdateCommand: newsletterUpdateCommand2 } = await Promise.resolve().then(() => (init_newsletter(), exports_newsletter));
|
|
7993
7757
|
await runNewsletterUpdate(id, options, newsletterUpdateCommand2);
|
|
7994
7758
|
});
|
|
7995
|
-
adminNewsletter.command("send").description("Send an edition
|
|
7759
|
+
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) => {
|
|
7996
7760
|
const { newsletterSendCommand: newsletterSendCommand2 } = await Promise.resolve().then(() => (init_newsletter(), exports_newsletter));
|
|
7997
7761
|
await runNewsletterSend(id, options, newsletterSendCommand2);
|
|
7998
7762
|
});
|
|
@@ -8000,7 +7764,7 @@ function addAdminCommands(program2) {
|
|
|
8000
7764
|
const { newsletterListCommand: newsletterListCommand2 } = await Promise.resolve().then(() => (init_newsletter(), exports_newsletter));
|
|
8001
7765
|
await runNewsletterList(options, newsletterListCommand2);
|
|
8002
7766
|
});
|
|
8003
|
-
adminNewsletter.command("delete").description("Delete an edition (draft or sent)
|
|
7767
|
+
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) => {
|
|
8004
7768
|
const { newsletterDeleteCommand: newsletterDeleteCommand2 } = await Promise.resolve().then(() => (init_newsletter(), exports_newsletter));
|
|
8005
7769
|
await runNewsletterDelete(id, options, newsletterDeleteCommand2);
|
|
8006
7770
|
});
|
|
@@ -8047,14 +7811,10 @@ function addAdminCommands(program2) {
|
|
|
8047
7811
|
const { authLastfmCommand: authLastfmCommand2 } = await Promise.resolve().then(() => (init_auth_lastfm(), exports_auth_lastfm));
|
|
8048
7812
|
await authLastfmCommand2(options);
|
|
8049
7813
|
});
|
|
8050
|
-
const artists = configureCommand(admin.command("artists").description("Artist entity +
|
|
7814
|
+
const artists = configureCommand(admin.command("artists").description("Artist entity + resolution commands"));
|
|
8051
7815
|
artists.action(() => {
|
|
8052
7816
|
artists.outputHelp();
|
|
8053
7817
|
});
|
|
8054
|
-
artists.command("follow").description("Auto-follow high-confidence artists on YouTube (drains the queue)").option("--dry-run", "Report who WOULD be followed without calling the platform", false).option("--limit <limit>", "Max artists to follow this run", "20").option("--json", "Print JSON", false).action(async (options) => {
|
|
8055
|
-
const { followArtistsCommand: followArtistsCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
|
|
8056
|
-
await runFollowArtists(options, followArtistsCommand2);
|
|
8057
|
-
});
|
|
8058
7818
|
const backfill = configureCommand(admin.command("backfills").description("Backfill operator-only archives"));
|
|
8059
7819
|
backfill.action(() => {
|
|
8060
7820
|
backfill.outputHelp();
|
|
@@ -8104,7 +7864,7 @@ async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCo
|
|
|
8104
7864
|
console.log(` mime: ${result.mime}`);
|
|
8105
7865
|
}
|
|
8106
7866
|
async function runTrackObserve(idOrLogId, options, trackObserveCommand2) {
|
|
8107
|
-
const script = options.scriptFile ?
|
|
7867
|
+
const script = options.scriptFile ? readFileSync5(options.scriptFile, "utf8") : options.script;
|
|
8108
7868
|
if (!idOrLogId || !script || !script.trim()) {
|
|
8109
7869
|
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]");
|
|
8110
7870
|
}
|
|
@@ -8160,7 +7920,7 @@ async function runTrackContext(idOrLogId, options, trackContextCommand2) {
|
|
|
8160
7920
|
}
|
|
8161
7921
|
}
|
|
8162
7922
|
async function runTrackNote(idOrLogId, options, trackNoteCommand2) {
|
|
8163
|
-
const note = options.scriptFile ?
|
|
7923
|
+
const note = options.scriptFile ? readFileSync5(options.scriptFile, "utf8") : options.script;
|
|
8164
7924
|
if (!idOrLogId || !note || !note.trim()) {
|
|
8165
7925
|
throw new Error("Usage: fluncle admin tracks note <track_id|log_id> (--script <text> | --script-file <file>) [--json]");
|
|
8166
7926
|
}
|
|
@@ -8349,7 +8109,7 @@ async function runArtistResolveQueue(options, listArtistsCommand2) {
|
|
|
8349
8109
|
return;
|
|
8350
8110
|
}
|
|
8351
8111
|
if (result.artists.length === 0) {
|
|
8352
|
-
console.log("
|
|
8112
|
+
console.log("Resolved every artist. Nothing waiting on the sweep.");
|
|
8353
8113
|
return;
|
|
8354
8114
|
}
|
|
8355
8115
|
const noun = result.artists.length === 1 ? "artist" : "artists";
|
|
@@ -8381,48 +8141,6 @@ async function runArtistResolve(artistId, options, resolveArtistCommand2) {
|
|
|
8381
8141
|
console.log(` wikidata: ${result.wikidataQid}`);
|
|
8382
8142
|
}
|
|
8383
8143
|
}
|
|
8384
|
-
async function runFollowArtists(options, followArtistsCommand2) {
|
|
8385
|
-
const limit = parseListLimit(options.limit);
|
|
8386
|
-
const followed = [];
|
|
8387
|
-
const failed = [];
|
|
8388
|
-
let dryRun = options.dryRun;
|
|
8389
|
-
let remaining = 0;
|
|
8390
|
-
while (followed.length + failed.length < limit) {
|
|
8391
|
-
const batchLimit = Math.min(50, limit - (followed.length + failed.length));
|
|
8392
|
-
const result = await followArtistsCommand2(batchLimit, options.dryRun);
|
|
8393
|
-
dryRun = result.dryRun;
|
|
8394
|
-
remaining = result.remaining;
|
|
8395
|
-
followed.push(...result.followed);
|
|
8396
|
-
failed.push(...result.failed);
|
|
8397
|
-
if (!options.json) {
|
|
8398
|
-
const verb2 = result.dryRun ? "would follow" : "followed";
|
|
8399
|
-
console.log(` \u2026${verb2} ${result.followedCount}; ${result.failedCount} failed; ${result.remaining} remaining`);
|
|
8400
|
-
}
|
|
8401
|
-
if (remaining === 0 || result.dryRun || result.followedCount === 0) {
|
|
8402
|
-
break;
|
|
8403
|
-
}
|
|
8404
|
-
}
|
|
8405
|
-
if (options.json) {
|
|
8406
|
-
printJson({
|
|
8407
|
-
dryRun,
|
|
8408
|
-
failed,
|
|
8409
|
-
failedCount: failed.length,
|
|
8410
|
-
followed,
|
|
8411
|
-
followedCount: followed.length,
|
|
8412
|
-
ok: true,
|
|
8413
|
-
remaining
|
|
8414
|
-
});
|
|
8415
|
-
return;
|
|
8416
|
-
}
|
|
8417
|
-
const verb = dryRun ? "Would follow" : "Followed";
|
|
8418
|
-
console.log(`${verb} ${followed.length} artist(s); ${failed.length} failed; ${remaining} remaining.`);
|
|
8419
|
-
for (const item of followed) {
|
|
8420
|
-
console.log(` ${item.platform}: ${item.artistName}`);
|
|
8421
|
-
}
|
|
8422
|
-
for (const item of failed) {
|
|
8423
|
-
console.log(` ${item.platform} ${item.socialId}: ${item.error}`);
|
|
8424
|
-
}
|
|
8425
|
-
}
|
|
8426
8144
|
async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
|
|
8427
8145
|
if (!idOrLogId) {
|
|
8428
8146
|
throw new Error("Missing id. Usage: fluncle admin tracks video <track_id|log_id> (--dir <dir> | --footage <file> [--footage-social <file>] [--footage-notext <file>] [--footage-landscape <file>] [--footage-landscape-social <file>] [--poster <file>] [--cover <file>] [--note <file>] [--composition <file>] [--props <file>] [--render <file>] [--intent <file>] [--metrics <file>] [--scene <file>] | --plate <file> [--plate-background <file>]) [--allow-partial]");
|
|
@@ -8433,7 +8151,7 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
|
|
|
8433
8151
|
return;
|
|
8434
8152
|
}
|
|
8435
8153
|
const candidate = path2.join(dir, name);
|
|
8436
|
-
return
|
|
8154
|
+
return existsSync5(candidate) ? candidate : undefined;
|
|
8437
8155
|
};
|
|
8438
8156
|
const resolveFile = (explicit, name) => {
|
|
8439
8157
|
if (explicit) {
|
|
@@ -8617,7 +8335,7 @@ async function runTrackUpdate(trackId, options, trackUpdateCommand2) {
|
|
|
8617
8335
|
if (bpm !== undefined && !Number.isFinite(bpm)) {
|
|
8618
8336
|
throw new Error(`Invalid --bpm: ${options.bpm}`);
|
|
8619
8337
|
}
|
|
8620
|
-
const embeddingRaw = options.embeddingFile ?
|
|
8338
|
+
const embeddingRaw = options.embeddingFile ? readFileSync5(options.embeddingFile, "utf8") : options.embedding;
|
|
8621
8339
|
const embedding = parseEmbeddingArg(embeddingRaw);
|
|
8622
8340
|
const result = await trackUpdateCommand2(trackId, {
|
|
8623
8341
|
bpm,
|
|
@@ -8643,7 +8361,7 @@ async function runTrackRequeueVideo(idOrLogId, options, trackRequeueVideoCommand
|
|
|
8643
8361
|
printJson(result);
|
|
8644
8362
|
return;
|
|
8645
8363
|
}
|
|
8646
|
-
console.log(result.alreadyClear ? `${result.logId} already had no video
|
|
8364
|
+
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).`);
|
|
8647
8365
|
}
|
|
8648
8366
|
async function runTrackPurgeVideo(idOrLogId, options, trackPurgeVideoCommand2) {
|
|
8649
8367
|
if (!idOrLogId) {
|
|
@@ -8654,7 +8372,7 @@ async function runTrackPurgeVideo(idOrLogId, options, trackPurgeVideoCommand2) {
|
|
|
8654
8372
|
printJson(result);
|
|
8655
8373
|
return;
|
|
8656
8374
|
}
|
|
8657
|
-
console.log(result.noVideo ? `${result.logId} has no video
|
|
8375
|
+
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.`);
|
|
8658
8376
|
}
|
|
8659
8377
|
async function runMixtapeUpdate(id, options, mixtapeUpdateCommand2) {
|
|
8660
8378
|
if (!id) {
|
|
@@ -8842,14 +8560,14 @@ async function runNewsletterSend(id, options, newsletterSendCommand2) {
|
|
|
8842
8560
|
return;
|
|
8843
8561
|
}
|
|
8844
8562
|
const number = result.edition.number;
|
|
8845
|
-
console.log(number === undefined ? `Sent edition ${result.edition.id}.` : `Sent edition #${number}
|
|
8563
|
+
console.log(number === undefined ? `Sent edition ${result.edition.id}.` : `Sent edition #${number}. It's out to the list and in the archive.`);
|
|
8846
8564
|
}
|
|
8847
8565
|
async function runNewsletterDelete(id, options, newsletterDeleteCommand2) {
|
|
8848
8566
|
if (!id) {
|
|
8849
8567
|
throw new Error("Missing edition id. Usage: fluncle admin newsletter delete <id> --yes");
|
|
8850
8568
|
}
|
|
8851
8569
|
if (!options.yes) {
|
|
8852
|
-
throw new Error("Pass --yes to confirm
|
|
8570
|
+
throw new Error("Pass --yes to confirm. This hard-deletes the edition (draft or sent).");
|
|
8853
8571
|
}
|
|
8854
8572
|
const result = await newsletterDeleteCommand2(id);
|
|
8855
8573
|
if (options.json) {
|
package/package.json
CHANGED