fluncle 0.92.0 → 0.94.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 +289 -37
- package/package.json +1 -1
package/bin/fluncle.mjs
CHANGED
|
@@ -556,7 +556,7 @@ function parseVersion(version) {
|
|
|
556
556
|
var currentVersion;
|
|
557
557
|
var init_version = __esm(() => {
|
|
558
558
|
init_output();
|
|
559
|
-
currentVersion = "0.
|
|
559
|
+
currentVersion = "0.94.0".trim() ? "0.94.0".trim() : "0.1.0";
|
|
560
560
|
});
|
|
561
561
|
|
|
562
562
|
// src/update-notifier.ts
|
|
@@ -2186,7 +2186,7 @@ function parseVersion2(version) {
|
|
|
2186
2186
|
var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
|
|
2187
2187
|
var init_version2 = __esm(() => {
|
|
2188
2188
|
init_output();
|
|
2189
|
-
currentVersion2 = "0.
|
|
2189
|
+
currentVersion2 = "0.94.0".trim() ? "0.94.0".trim() : "0.1.0";
|
|
2190
2190
|
});
|
|
2191
2191
|
|
|
2192
2192
|
// ../../packages/registry/src/index.ts
|
|
@@ -3144,6 +3144,224 @@ var init_track = __esm(() => {
|
|
|
3144
3144
|
NON_FILE_OPTIONS = ["model", "reasoning"];
|
|
3145
3145
|
});
|
|
3146
3146
|
|
|
3147
|
+
// src/commands/helm.ts
|
|
3148
|
+
var exports_helm = {};
|
|
3149
|
+
__export(exports_helm, {
|
|
3150
|
+
resolveHelmDir: () => resolveHelmDir,
|
|
3151
|
+
helmUninstallCommand: () => helmUninstallCommand,
|
|
3152
|
+
helmOpenCommand: () => helmOpenCommand,
|
|
3153
|
+
helmInstallCommand: () => helmInstallCommand,
|
|
3154
|
+
buildLaunchAgentPlist: () => buildLaunchAgentPlist
|
|
3155
|
+
});
|
|
3156
|
+
import { existsSync, mkdirSync as mkdirSync2, openSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3157
|
+
import { homedir as homedir4 } from "node:os";
|
|
3158
|
+
import { dirname as dirname3, join as join4, resolve } from "node:path";
|
|
3159
|
+
function launchAgentPlistPath() {
|
|
3160
|
+
return join4(homedir4(), "Library/LaunchAgents", `${LAUNCH_AGENT_LABEL}.plist`);
|
|
3161
|
+
}
|
|
3162
|
+
function helmLogPath() {
|
|
3163
|
+
return join4(homedir4(), "Library/Logs/fluncle-helm.log");
|
|
3164
|
+
}
|
|
3165
|
+
function resolveHelmDir() {
|
|
3166
|
+
const fromEnv = process.env.FLUNCLE_HELM_DIR;
|
|
3167
|
+
if (fromEnv) {
|
|
3168
|
+
if (!existsSync(join4(fromEnv, "src/server.ts"))) {
|
|
3169
|
+
throw new CliError2("helm_not_found", `FLUNCLE_HELM_DIR points at ${fromEnv}, but there's no helm there (src/server.ts missing).`);
|
|
3170
|
+
}
|
|
3171
|
+
return resolve(fromEnv);
|
|
3172
|
+
}
|
|
3173
|
+
let dir = import.meta.dir;
|
|
3174
|
+
for (let depth = 0;depth < 8; depth++) {
|
|
3175
|
+
const candidate = join4(dir, "apps/helm");
|
|
3176
|
+
if (existsSync(join4(candidate, "src/server.ts"))) {
|
|
3177
|
+
return candidate;
|
|
3178
|
+
}
|
|
3179
|
+
const parent = dirname3(dir);
|
|
3180
|
+
if (parent === dir) {
|
|
3181
|
+
break;
|
|
3182
|
+
}
|
|
3183
|
+
dir = parent;
|
|
3184
|
+
}
|
|
3185
|
+
throw new CliError2("helm_not_found", "No helm aboard. Run this from the fluncle repo, or set FLUNCLE_HELM_DIR to the apps/helm checkout.");
|
|
3186
|
+
}
|
|
3187
|
+
function resolveBun() {
|
|
3188
|
+
const bun = Bun.which("bun");
|
|
3189
|
+
if (!bun) {
|
|
3190
|
+
throw new CliError2("no_bun", "No bun on the PATH. The helm daemon runs under bun.");
|
|
3191
|
+
}
|
|
3192
|
+
return bun;
|
|
3193
|
+
}
|
|
3194
|
+
async function daemonHealthy() {
|
|
3195
|
+
try {
|
|
3196
|
+
const response = await fetch(`${HELM_URL}/api/health`, {
|
|
3197
|
+
signal: AbortSignal.timeout(1500)
|
|
3198
|
+
});
|
|
3199
|
+
return response.ok;
|
|
3200
|
+
} catch {
|
|
3201
|
+
return false;
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
async function ensureBuilt(helmDir) {
|
|
3205
|
+
if (existsSync(join4(helmDir, "dist/index.html"))) {
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
console.log("The glass isn't built yet. Building it once…");
|
|
3209
|
+
const proc = Bun.spawn([resolveBun(), "run", "build"], {
|
|
3210
|
+
cwd: helmDir,
|
|
3211
|
+
stderr: "inherit",
|
|
3212
|
+
stdin: "ignore",
|
|
3213
|
+
stdout: "inherit"
|
|
3214
|
+
});
|
|
3215
|
+
const exitCode = await proc.exited;
|
|
3216
|
+
if (exitCode !== 0) {
|
|
3217
|
+
throw new CliError2("helm_build_failed", `The glass build failed (exit ${exitCode}).`);
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
function bootDaemon(helmDir) {
|
|
3221
|
+
const logPath = helmLogPath();
|
|
3222
|
+
mkdirSync2(dirname3(logPath), { recursive: true });
|
|
3223
|
+
const logFd = openSync(logPath, "a");
|
|
3224
|
+
const proc = Bun.spawn([resolveBun(), "src/server.ts"], {
|
|
3225
|
+
cwd: helmDir,
|
|
3226
|
+
stderr: logFd,
|
|
3227
|
+
stdin: "ignore",
|
|
3228
|
+
stdout: logFd
|
|
3229
|
+
});
|
|
3230
|
+
proc.unref();
|
|
3231
|
+
}
|
|
3232
|
+
async function waitForHealth(timeoutMs) {
|
|
3233
|
+
const deadline = Date.now() + timeoutMs;
|
|
3234
|
+
while (Date.now() < deadline) {
|
|
3235
|
+
if (await daemonHealthy()) {
|
|
3236
|
+
return true;
|
|
3237
|
+
}
|
|
3238
|
+
await Bun.sleep(250);
|
|
3239
|
+
}
|
|
3240
|
+
return false;
|
|
3241
|
+
}
|
|
3242
|
+
async function openAppWindow() {
|
|
3243
|
+
if (process.platform === "darwin" && chromeInstalled()) {
|
|
3244
|
+
const proc = Bun.spawn(["open", "-na", "Google Chrome", "--args", `--app=${HELM_URL}`], {
|
|
3245
|
+
stderr: "ignore",
|
|
3246
|
+
stdin: "ignore",
|
|
3247
|
+
stdout: "ignore"
|
|
3248
|
+
});
|
|
3249
|
+
if (await proc.exited === 0) {
|
|
3250
|
+
return;
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
await openExternal(HELM_URL);
|
|
3254
|
+
}
|
|
3255
|
+
function chromeInstalled() {
|
|
3256
|
+
return existsSync("/Applications/Google Chrome.app") || existsSync(join4(homedir4(), "Applications/Google Chrome.app"));
|
|
3257
|
+
}
|
|
3258
|
+
async function helmOpenCommand() {
|
|
3259
|
+
if (await daemonHealthy()) {
|
|
3260
|
+
console.log(`The helm holds on :${HELM_PORT}. Opening the window.`);
|
|
3261
|
+
await openAppWindow();
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
const helmDir = resolveHelmDir();
|
|
3265
|
+
await ensureBuilt(helmDir);
|
|
3266
|
+
bootDaemon(helmDir);
|
|
3267
|
+
if (!await waitForHealth(15000)) {
|
|
3268
|
+
throw new CliError2("helm_no_answer", `The daemon didn't answer on :${HELM_PORT}. Its log is at ${helmLogPath()}.`);
|
|
3269
|
+
}
|
|
3270
|
+
console.log(`Helm raised on :${HELM_PORT}. Opening the window.`);
|
|
3271
|
+
await openAppWindow();
|
|
3272
|
+
}
|
|
3273
|
+
function xmlEscape(text) {
|
|
3274
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
3275
|
+
}
|
|
3276
|
+
function buildLaunchAgentPlist(helmDir, bunPath, logPath) {
|
|
3277
|
+
const path2 = `${dirname3(bunPath)}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`;
|
|
3278
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3279
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3280
|
+
<plist version="1.0">
|
|
3281
|
+
<dict>
|
|
3282
|
+
<key>EnvironmentVariables</key>
|
|
3283
|
+
<dict>
|
|
3284
|
+
<key>PATH</key>
|
|
3285
|
+
<string>${xmlEscape(path2)}</string>
|
|
3286
|
+
</dict>
|
|
3287
|
+
<key>KeepAlive</key>
|
|
3288
|
+
<dict>
|
|
3289
|
+
<key>SuccessfulExit</key>
|
|
3290
|
+
<false/>
|
|
3291
|
+
</dict>
|
|
3292
|
+
<key>Label</key>
|
|
3293
|
+
<string>${LAUNCH_AGENT_LABEL}</string>
|
|
3294
|
+
<key>ProgramArguments</key>
|
|
3295
|
+
<array>
|
|
3296
|
+
<string>${xmlEscape(bunPath)}</string>
|
|
3297
|
+
<string>src/server.ts</string>
|
|
3298
|
+
</array>
|
|
3299
|
+
<key>RunAtLoad</key>
|
|
3300
|
+
<true/>
|
|
3301
|
+
<key>StandardErrorPath</key>
|
|
3302
|
+
<string>${xmlEscape(logPath)}</string>
|
|
3303
|
+
<key>StandardOutPath</key>
|
|
3304
|
+
<string>${xmlEscape(logPath)}</string>
|
|
3305
|
+
<key>WorkingDirectory</key>
|
|
3306
|
+
<string>${xmlEscape(helmDir)}</string>
|
|
3307
|
+
</dict>
|
|
3308
|
+
</plist>
|
|
3309
|
+
`;
|
|
3310
|
+
}
|
|
3311
|
+
function currentUid() {
|
|
3312
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
3313
|
+
if (uid === undefined) {
|
|
3314
|
+
throw new CliError2("no_uid", "Couldn't read the user id — launchd needs a gui domain.");
|
|
3315
|
+
}
|
|
3316
|
+
return uid;
|
|
3317
|
+
}
|
|
3318
|
+
async function launchctl(args) {
|
|
3319
|
+
const proc = Bun.spawn(["launchctl", ...args], {
|
|
3320
|
+
stderr: "ignore",
|
|
3321
|
+
stdin: "ignore",
|
|
3322
|
+
stdout: "ignore"
|
|
3323
|
+
});
|
|
3324
|
+
return proc.exited;
|
|
3325
|
+
}
|
|
3326
|
+
async function helmInstallCommand() {
|
|
3327
|
+
if (process.platform !== "darwin") {
|
|
3328
|
+
throw new CliError2("unsupported_platform", "launchd is a macOS thing. Nothing to install here.");
|
|
3329
|
+
}
|
|
3330
|
+
const helmDir = resolveHelmDir();
|
|
3331
|
+
await ensureBuilt(helmDir);
|
|
3332
|
+
const plistPath = launchAgentPlistPath();
|
|
3333
|
+
mkdirSync2(dirname3(plistPath), { recursive: true });
|
|
3334
|
+
mkdirSync2(dirname3(helmLogPath()), { recursive: true });
|
|
3335
|
+
writeFileSync2(plistPath, buildLaunchAgentPlist(helmDir, resolveBun(), helmLogPath()));
|
|
3336
|
+
const uid = currentUid();
|
|
3337
|
+
await launchctl(["bootout", `gui/${uid}/${LAUNCH_AGENT_LABEL}`]);
|
|
3338
|
+
const exitCode = await launchctl(["bootstrap", `gui/${uid}`, plistPath]);
|
|
3339
|
+
if (exitCode !== 0) {
|
|
3340
|
+
throw new CliError2("launchctl_refused", `launchctl bootstrap refused (exit ${exitCode}). The plist is at ${plistPath}.`);
|
|
3341
|
+
}
|
|
3342
|
+
console.log(`LaunchAgent installed (${LAUNCH_AGENT_LABEL}).`);
|
|
3343
|
+
console.log(`The daemon rises at login and holds :${HELM_PORT}. Log: ${helmLogPath()}`);
|
|
3344
|
+
}
|
|
3345
|
+
async function helmUninstallCommand() {
|
|
3346
|
+
if (process.platform !== "darwin") {
|
|
3347
|
+
throw new CliError2("unsupported_platform", "launchd is a macOS thing. Nothing to remove here.");
|
|
3348
|
+
}
|
|
3349
|
+
const plistPath = launchAgentPlistPath();
|
|
3350
|
+
await launchctl(["bootout", `gui/${currentUid()}/${LAUNCH_AGENT_LABEL}`]);
|
|
3351
|
+
if (existsSync(plistPath)) {
|
|
3352
|
+
rmSync2(plistPath);
|
|
3353
|
+
console.log("LaunchAgent removed and the daemon stood down.");
|
|
3354
|
+
return;
|
|
3355
|
+
}
|
|
3356
|
+
console.log("No LaunchAgent on file. Nothing stood down.");
|
|
3357
|
+
}
|
|
3358
|
+
var HELM_PORT = 4190, HELM_URL, LAUNCH_AGENT_LABEL = "com.fluncle.helm";
|
|
3359
|
+
var init_helm = __esm(() => {
|
|
3360
|
+
init_open_external();
|
|
3361
|
+
init_output();
|
|
3362
|
+
HELM_URL = `http://127.0.0.1:${HELM_PORT}`;
|
|
3363
|
+
});
|
|
3364
|
+
|
|
3147
3365
|
// src/commands/add.ts
|
|
3148
3366
|
var exports_add = {};
|
|
3149
3367
|
__export(exports_add, {
|
|
@@ -3675,9 +3893,9 @@ var init_mixtape_youtube2 = __esm(() => {
|
|
|
3675
3893
|
|
|
3676
3894
|
// src/commands/mixtape-set-video.ts
|
|
3677
3895
|
import { randomUUID } from "node:crypto";
|
|
3678
|
-
import { existsSync, rmSync as
|
|
3896
|
+
import { existsSync as existsSync2, rmSync as rmSync3, statSync as statSync3 } from "node:fs";
|
|
3679
3897
|
import { tmpdir } from "node:os";
|
|
3680
|
-
import { join as
|
|
3898
|
+
import { join as join5 } from "node:path";
|
|
3681
3899
|
function planMultipart(contentLength, partSize = DEFAULT_PART_SIZE) {
|
|
3682
3900
|
if (!Number.isInteger(contentLength) || contentLength <= 0) {
|
|
3683
3901
|
throw new CliError2("invalid_size", `The rendition size must be a positive integer (got ${contentLength})`);
|
|
@@ -3732,11 +3950,11 @@ function renditionFfmpegArgs(inputPath, outputPath) {
|
|
|
3732
3950
|
];
|
|
3733
3951
|
}
|
|
3734
3952
|
async function uploadRenditionMultipart(masterPath, presignPath, onProgress = () => {}) {
|
|
3735
|
-
if (!
|
|
3953
|
+
if (!existsSync2(masterPath)) {
|
|
3736
3954
|
throw new CliError2("file_not_found", `Set-video master not found: ${masterPath}`);
|
|
3737
3955
|
}
|
|
3738
3956
|
await assertFfmpeg();
|
|
3739
|
-
const renditionPath =
|
|
3957
|
+
const renditionPath = join5(tmpdir(), `fluncle-set-${randomUUID()}.mp4`);
|
|
3740
3958
|
onProgress("Set video: deriving the 1080p faststart rendition (ffmpeg)…");
|
|
3741
3959
|
await deriveRendition(masterPath, renditionPath);
|
|
3742
3960
|
try {
|
|
@@ -3755,7 +3973,7 @@ async function uploadRenditionMultipart(masterPath, presignPath, onProgress = ()
|
|
|
3755
3973
|
throw new CliError2("presign_missing", `Worker did not sign part ${part.partNumber} of ${plan.partCount}`);
|
|
3756
3974
|
}
|
|
3757
3975
|
onProgress(`Set video: part ${part.partNumber}/${plan.partCount}`);
|
|
3758
|
-
const etag = await putPart(url, renditionPath, part);
|
|
3976
|
+
const etag = await putPart(url, renditionPath, part, onProgress);
|
|
3759
3977
|
completed.push({ etag, partNumber: part.partNumber });
|
|
3760
3978
|
}
|
|
3761
3979
|
await completeUpload(presign.completeUrl, completed);
|
|
@@ -3765,17 +3983,35 @@ async function uploadRenditionMultipart(masterPath, presignPath, onProgress = ()
|
|
|
3765
3983
|
}
|
|
3766
3984
|
return { key: presign.key, url: `${FOUND_BASE2}/${presign.key}` };
|
|
3767
3985
|
} finally {
|
|
3768
|
-
|
|
3986
|
+
rmSync3(renditionPath, { force: true });
|
|
3769
3987
|
}
|
|
3770
3988
|
}
|
|
3771
|
-
async function putPart(url, path2, part) {
|
|
3772
|
-
const
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3989
|
+
async function putPart(url, path2, part, onProgress = () => {}) {
|
|
3990
|
+
const body = await Bun.file(path2).slice(part.start, part.end).arrayBuffer();
|
|
3991
|
+
for (let attempt = 1;; attempt += 1) {
|
|
3992
|
+
try {
|
|
3993
|
+
return await putPartOnce(url, body, part);
|
|
3994
|
+
} catch (error) {
|
|
3995
|
+
if (error instanceof CliError2) {
|
|
3996
|
+
throw error;
|
|
3997
|
+
}
|
|
3998
|
+
if (attempt >= MAX_PART_ATTEMPTS) {
|
|
3999
|
+
throw new CliError2("r2_part_failed", `Part ${part.partNumber} failed after ${MAX_PART_ATTEMPTS} attempts: ${error instanceof Error ? error.message : String(error)}`);
|
|
4000
|
+
}
|
|
4001
|
+
const backoffMs = 500 * 2 ** (attempt - 1);
|
|
4002
|
+
onProgress(`Set video: part ${part.partNumber} dropped — retry ${attempt}/${MAX_PART_ATTEMPTS - 1} in ${backoffMs}ms…`);
|
|
4003
|
+
await new Promise((resolve2) => setTimeout(resolve2, backoffMs));
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
async function putPartOnce(url, body, part) {
|
|
4008
|
+
const response = await fetch(url, { body, method: "PUT" });
|
|
3776
4009
|
if (!response.ok) {
|
|
3777
4010
|
const detail = (await response.text().catch(() => "")).slice(0, 300);
|
|
3778
|
-
|
|
4011
|
+
if (response.status < 500) {
|
|
4012
|
+
throw new CliError2("r2_part_failed", `R2 rejected part ${part.partNumber} (${response.status} ${response.statusText})${detail ? `: ${detail}` : ""}`);
|
|
4013
|
+
}
|
|
4014
|
+
throw new Error(`R2 ${response.status} ${response.statusText} on part ${part.partNumber}${detail ? `: ${detail}` : ""}`);
|
|
3779
4015
|
}
|
|
3780
4016
|
const etag = response.headers.get("etag");
|
|
3781
4017
|
if (!etag) {
|
|
@@ -3819,7 +4055,7 @@ async function deriveRendition(inputPath, outputPath) {
|
|
|
3819
4055
|
throw new CliError2("ffmpeg_failed", `ffmpeg failed to derive the set-video rendition${detail ? `: ${detail}` : ""}`);
|
|
3820
4056
|
}
|
|
3821
4057
|
}
|
|
3822
|
-
var FOUND_BASE2 = "https://found.fluncle.com", SET_VIDEO_RENDITION, DEFAULT_PART_SIZE, MIN_PART_SIZE, MAX_PARTS = 1e4;
|
|
4058
|
+
var FOUND_BASE2 = "https://found.fluncle.com", SET_VIDEO_RENDITION, DEFAULT_PART_SIZE, MIN_PART_SIZE, MAX_PARTS = 1e4, MAX_PART_ATTEMPTS = 5;
|
|
3823
4059
|
var init_mixtape_set_video = __esm(() => {
|
|
3824
4060
|
init_api();
|
|
3825
4061
|
init_output();
|
|
@@ -3829,7 +4065,7 @@ var init_mixtape_set_video = __esm(() => {
|
|
|
3829
4065
|
gopSeconds: 2,
|
|
3830
4066
|
height: 1080
|
|
3831
4067
|
};
|
|
3832
|
-
DEFAULT_PART_SIZE =
|
|
4068
|
+
DEFAULT_PART_SIZE = 16 * 1024 * 1024;
|
|
3833
4069
|
MIN_PART_SIZE = 5 * 1024 * 1024;
|
|
3834
4070
|
});
|
|
3835
4071
|
|
|
@@ -3845,7 +4081,7 @@ __export(exports_recordings, {
|
|
|
3845
4081
|
recordingDeleteCommand: () => recordingDeleteCommand,
|
|
3846
4082
|
recordingCreateCommand: () => recordingCreateCommand
|
|
3847
4083
|
});
|
|
3848
|
-
import { existsSync as
|
|
4084
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
3849
4085
|
async function recordingGet(id) {
|
|
3850
4086
|
const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
3851
4087
|
return response.recording;
|
|
@@ -3914,7 +4150,7 @@ async function recordingCreateCommand(options = {}) {
|
|
|
3914
4150
|
if (!options.video) {
|
|
3915
4151
|
throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
|
|
3916
4152
|
}
|
|
3917
|
-
if (!
|
|
4153
|
+
if (!existsSync3(options.video)) {
|
|
3918
4154
|
throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
|
|
3919
4155
|
}
|
|
3920
4156
|
const created = await adminApiPost("/api/admin/recordings", {
|
|
@@ -3950,7 +4186,7 @@ async function recordingUpdateCommand(id, options = {}) {
|
|
|
3950
4186
|
body.parentId = options.parentId;
|
|
3951
4187
|
}
|
|
3952
4188
|
if (options.tracklistFile !== undefined) {
|
|
3953
|
-
if (!
|
|
4189
|
+
if (!existsSync3(options.tracklistFile)) {
|
|
3954
4190
|
throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
|
|
3955
4191
|
}
|
|
3956
4192
|
try {
|
|
@@ -3999,7 +4235,7 @@ async function recordingReplaceCuesCommand(id, options = {}) {
|
|
|
3999
4235
|
if (!options.cuesFile) {
|
|
4000
4236
|
throw new CliError2("missing_cues_file", "replace-cues needs a --cues-file <file> (the ordered cue array)");
|
|
4001
4237
|
}
|
|
4002
|
-
if (!
|
|
4238
|
+
if (!existsSync3(options.cuesFile)) {
|
|
4003
4239
|
throw new CliError2("file_not_found", `Cues file not found: ${options.cuesFile}`);
|
|
4004
4240
|
}
|
|
4005
4241
|
let cues;
|
|
@@ -4039,9 +4275,9 @@ __export(exports_clips, {
|
|
|
4039
4275
|
CLIP_AUDIO_BITRATE: () => CLIP_AUDIO_BITRATE
|
|
4040
4276
|
});
|
|
4041
4277
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4042
|
-
import { rmSync as
|
|
4278
|
+
import { rmSync as rmSync4, statSync as statSync4 } from "node:fs";
|
|
4043
4279
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
4044
|
-
import { join as
|
|
4280
|
+
import { join as join6 } from "node:path";
|
|
4045
4281
|
function clipFootageKey(clipId) {
|
|
4046
4282
|
return `${clipId}/footage.mp4`;
|
|
4047
4283
|
}
|
|
@@ -4124,7 +4360,7 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4124
4360
|
}
|
|
4125
4361
|
const source = await resolveClipSource(clip);
|
|
4126
4362
|
await assertFfmpeg2();
|
|
4127
|
-
const outputPath =
|
|
4363
|
+
const outputPath = join6(tmpdir2(), `fluncle-clip-${randomUUID2()}.mp4`);
|
|
4128
4364
|
try {
|
|
4129
4365
|
onProgress(`Clip ${clipId}: cutting [${clip.inMs}–${clip.outMs}ms]…`);
|
|
4130
4366
|
await runClipCut({
|
|
@@ -4150,7 +4386,7 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4150
4386
|
url: `${FOUND_BASE3}/${presign.key}`
|
|
4151
4387
|
};
|
|
4152
4388
|
} finally {
|
|
4153
|
-
|
|
4389
|
+
rmSync4(outputPath, { force: true });
|
|
4154
4390
|
}
|
|
4155
4391
|
}
|
|
4156
4392
|
async function putClip(url, contentType, path2) {
|
|
@@ -4208,7 +4444,7 @@ __export(exports_recordings2, {
|
|
|
4208
4444
|
recordingDeleteCommand: () => recordingDeleteCommand2,
|
|
4209
4445
|
recordingCreateCommand: () => recordingCreateCommand2
|
|
4210
4446
|
});
|
|
4211
|
-
import { existsSync as
|
|
4447
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
|
|
4212
4448
|
async function recordingGet2(id) {
|
|
4213
4449
|
const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
4214
4450
|
return response.recording;
|
|
@@ -4277,7 +4513,7 @@ async function recordingCreateCommand2(options = {}) {
|
|
|
4277
4513
|
if (!options.video) {
|
|
4278
4514
|
throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
|
|
4279
4515
|
}
|
|
4280
|
-
if (!
|
|
4516
|
+
if (!existsSync4(options.video)) {
|
|
4281
4517
|
throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
|
|
4282
4518
|
}
|
|
4283
4519
|
const created = await adminApiPost("/api/admin/recordings", {
|
|
@@ -4313,7 +4549,7 @@ async function recordingUpdateCommand2(id, options = {}) {
|
|
|
4313
4549
|
body.parentId = options.parentId;
|
|
4314
4550
|
}
|
|
4315
4551
|
if (options.tracklistFile !== undefined) {
|
|
4316
|
-
if (!
|
|
4552
|
+
if (!existsSync4(options.tracklistFile)) {
|
|
4317
4553
|
throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
|
|
4318
4554
|
}
|
|
4319
4555
|
try {
|
|
@@ -4362,7 +4598,7 @@ async function recordingReplaceCuesCommand2(id, options = {}) {
|
|
|
4362
4598
|
if (!options.cuesFile) {
|
|
4363
4599
|
throw new CliError2("missing_cues_file", "replace-cues needs a --cues-file <file> (the ordered cue array)");
|
|
4364
4600
|
}
|
|
4365
|
-
if (!
|
|
4601
|
+
if (!existsSync4(options.cuesFile)) {
|
|
4366
4602
|
throw new CliError2("file_not_found", `Cues file not found: ${options.cuesFile}`);
|
|
4367
4603
|
}
|
|
4368
4604
|
let cues;
|
|
@@ -4393,7 +4629,7 @@ __export(exports_newsletter, {
|
|
|
4393
4629
|
newsletterDraftCommand: () => newsletterDraftCommand,
|
|
4394
4630
|
newsletterDeleteCommand: () => newsletterDeleteCommand
|
|
4395
4631
|
});
|
|
4396
|
-
import { existsSync as
|
|
4632
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
|
|
4397
4633
|
function buildBody2(options, { requireContent }) {
|
|
4398
4634
|
const body = {};
|
|
4399
4635
|
if (options.contentFile !== undefined) {
|
|
@@ -4413,7 +4649,7 @@ function buildBody2(options, { requireContent }) {
|
|
|
4413
4649
|
return body;
|
|
4414
4650
|
}
|
|
4415
4651
|
function readContentFile(filePath) {
|
|
4416
|
-
if (!
|
|
4652
|
+
if (!existsSync5(filePath)) {
|
|
4417
4653
|
throw new CliError2("file_not_found", `Content file not found: ${filePath}`);
|
|
4418
4654
|
}
|
|
4419
4655
|
const text = readFileSync4(filePath, "utf-8");
|
|
@@ -4805,7 +5041,7 @@ async function selectWithKeyboard2(items, options) {
|
|
|
4805
5041
|
let renderedLines = 0;
|
|
4806
5042
|
let done = false;
|
|
4807
5043
|
const wasRaw = stdin.isRaw === true;
|
|
4808
|
-
return await new Promise((
|
|
5044
|
+
return await new Promise((resolve2) => {
|
|
4809
5045
|
function cleanup() {
|
|
4810
5046
|
if (done) {
|
|
4811
5047
|
return;
|
|
@@ -4820,14 +5056,14 @@ async function selectWithKeyboard2(items, options) {
|
|
|
4820
5056
|
cleanup();
|
|
4821
5057
|
stdout.write(renderedLines > 0 ? `
|
|
4822
5058
|
` : "");
|
|
4823
|
-
|
|
5059
|
+
resolve2(item);
|
|
4824
5060
|
}
|
|
4825
5061
|
function cancel() {
|
|
4826
5062
|
cleanup();
|
|
4827
5063
|
clearRendered2(stdout, renderedLines);
|
|
4828
5064
|
stdout.write(`Cancelled.
|
|
4829
5065
|
`);
|
|
4830
|
-
|
|
5066
|
+
resolve2(undefined);
|
|
4831
5067
|
}
|
|
4832
5068
|
function render() {
|
|
4833
5069
|
clearRendered2(stdout, renderedLines);
|
|
@@ -4883,7 +5119,7 @@ async function paginateWithKeyboard(options) {
|
|
|
4883
5119
|
let busy = false;
|
|
4884
5120
|
let done = false;
|
|
4885
5121
|
const wasRaw = stdin.isRaw === true;
|
|
4886
|
-
return await new Promise((
|
|
5122
|
+
return await new Promise((resolve2) => {
|
|
4887
5123
|
function cleanup() {
|
|
4888
5124
|
if (done) {
|
|
4889
5125
|
return;
|
|
@@ -4898,7 +5134,7 @@ async function paginateWithKeyboard(options) {
|
|
|
4898
5134
|
cleanup();
|
|
4899
5135
|
stdout.write(`
|
|
4900
5136
|
`);
|
|
4901
|
-
|
|
5137
|
+
resolve2();
|
|
4902
5138
|
}
|
|
4903
5139
|
function render() {
|
|
4904
5140
|
clearRendered2(stdout, renderedLines);
|
|
@@ -5051,7 +5287,7 @@ var init_format2 = __esm(() => {
|
|
|
5051
5287
|
});
|
|
5052
5288
|
|
|
5053
5289
|
// src/cli.ts
|
|
5054
|
-
import { existsSync as
|
|
5290
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
5055
5291
|
import path2 from "path";
|
|
5056
5292
|
|
|
5057
5293
|
// ../../node_modules/commander/lib/error.js
|
|
@@ -7197,6 +7433,7 @@ ${fluncleAsciiLogo}
|
|
|
7197
7433
|
addMetaCommands(program2);
|
|
7198
7434
|
addTrackCommands(program2);
|
|
7199
7435
|
addAdminCommands(program2);
|
|
7436
|
+
addHelmCommands(program2);
|
|
7200
7437
|
return program2;
|
|
7201
7438
|
}
|
|
7202
7439
|
async function main(args = process.argv.slice(2)) {
|
|
@@ -7295,6 +7532,21 @@ function addTrackCommands(program2) {
|
|
|
7295
7532
|
await runTrackGet(idOrLogId, options, trackGetCommand2);
|
|
7296
7533
|
});
|
|
7297
7534
|
}
|
|
7535
|
+
function addHelmCommands(program2) {
|
|
7536
|
+
const helm = configureCommand(program2.command("helm", { hidden: true }).description("Fluncle's Helm \u2014 the operator's mission control"));
|
|
7537
|
+
helm.action(async () => {
|
|
7538
|
+
const { helmOpenCommand: helmOpenCommand2 } = await Promise.resolve().then(() => (init_helm(), exports_helm));
|
|
7539
|
+
await helmOpenCommand2();
|
|
7540
|
+
});
|
|
7541
|
+
helm.command("install").description("Install the launchd LaunchAgent (the daemon rises at login)").action(async () => {
|
|
7542
|
+
const { helmInstallCommand: helmInstallCommand2 } = await Promise.resolve().then(() => (init_helm(), exports_helm));
|
|
7543
|
+
await helmInstallCommand2();
|
|
7544
|
+
});
|
|
7545
|
+
helm.command("uninstall").description("Remove the LaunchAgent and stand the daemon down").action(async () => {
|
|
7546
|
+
const { helmUninstallCommand: helmUninstallCommand2 } = await Promise.resolve().then(() => (init_helm(), exports_helm));
|
|
7547
|
+
await helmUninstallCommand2();
|
|
7548
|
+
});
|
|
7549
|
+
}
|
|
7298
7550
|
function addAdminCommands(program2) {
|
|
7299
7551
|
const admin = configureCommand(program2.command("admin", { hidden: true }).description("Operator commands"));
|
|
7300
7552
|
admin.action(() => {
|
|
@@ -7774,7 +8026,7 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
|
|
|
7774
8026
|
return;
|
|
7775
8027
|
}
|
|
7776
8028
|
const candidate = path2.join(dir, name);
|
|
7777
|
-
return
|
|
8029
|
+
return existsSync6(candidate) ? candidate : undefined;
|
|
7778
8030
|
};
|
|
7779
8031
|
const resolveFile = (explicit, name) => {
|
|
7780
8032
|
if (explicit) {
|
package/package.json
CHANGED