fluncle 0.93.0 → 0.95.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/fluncle.mjs +292 -36
  2. 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.93.0".trim() ? "0.93.0".trim() : "0.1.0";
559
+ currentVersion = "0.95.0".trim() ? "0.95.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.93.0".trim() ? "0.93.0".trim() : "0.1.0";
2189
+ currentVersion2 = "0.95.0".trim() ? "0.95.0".trim() : "0.1.0";
2190
2190
  });
2191
2191
 
2192
2192
  // ../../packages/registry/src/index.ts
@@ -3144,6 +3144,246 @@ 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, readFileSync as readFileSync2 } 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 walkUpForHelm(start) {
3166
+ let dir = start;
3167
+ for (let depth = 0;depth < 8; depth++) {
3168
+ const candidate = join4(dir, "apps/helm");
3169
+ if (existsSync(join4(candidate, "src/server.ts"))) {
3170
+ return candidate;
3171
+ }
3172
+ const parent = dirname3(dir);
3173
+ if (parent === dir) {
3174
+ break;
3175
+ }
3176
+ dir = parent;
3177
+ }
3178
+ return null;
3179
+ }
3180
+ function persistHelmDir(dir) {
3181
+ try {
3182
+ mkdirSync2(dirname3(HELM_DIR_FILE), { recursive: true });
3183
+ writeFileSync2(HELM_DIR_FILE, `${dir}
3184
+ `, { mode: 384 });
3185
+ } catch {}
3186
+ }
3187
+ function resolveHelmDir() {
3188
+ const fromEnv = process.env.FLUNCLE_HELM_DIR;
3189
+ if (fromEnv) {
3190
+ if (!existsSync(join4(fromEnv, "src/server.ts"))) {
3191
+ throw new CliError2("helm_not_found", `FLUNCLE_HELM_DIR points at ${fromEnv}, but there's no helm there (src/server.ts missing).`);
3192
+ }
3193
+ return resolve(fromEnv);
3194
+ }
3195
+ const found = walkUpForHelm(process.cwd()) ?? walkUpForHelm(import.meta.dir);
3196
+ if (found) {
3197
+ persistHelmDir(found);
3198
+ return found;
3199
+ }
3200
+ try {
3201
+ const remembered = readFileSync2(HELM_DIR_FILE, "utf8").trim();
3202
+ if (remembered && existsSync(join4(remembered, "src/server.ts"))) {
3203
+ return remembered;
3204
+ }
3205
+ } catch {}
3206
+ 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.");
3207
+ }
3208
+ function resolveBun() {
3209
+ const bun = Bun.which("bun");
3210
+ if (!bun) {
3211
+ throw new CliError2("no_bun", "No bun on the PATH. The helm daemon runs under bun.");
3212
+ }
3213
+ return bun;
3214
+ }
3215
+ async function daemonHealthy() {
3216
+ try {
3217
+ const response = await fetch(`${HELM_URL}/api/health`, {
3218
+ signal: AbortSignal.timeout(1500)
3219
+ });
3220
+ return response.ok;
3221
+ } catch {
3222
+ return false;
3223
+ }
3224
+ }
3225
+ async function ensureBuilt(helmDir) {
3226
+ if (existsSync(join4(helmDir, "dist/index.html"))) {
3227
+ return;
3228
+ }
3229
+ console.log("The glass isn't built yet. Building it once…");
3230
+ const proc = Bun.spawn([resolveBun(), "run", "build"], {
3231
+ cwd: helmDir,
3232
+ stderr: "inherit",
3233
+ stdin: "ignore",
3234
+ stdout: "inherit"
3235
+ });
3236
+ const exitCode = await proc.exited;
3237
+ if (exitCode !== 0) {
3238
+ throw new CliError2("helm_build_failed", `The glass build failed (exit ${exitCode}).`);
3239
+ }
3240
+ }
3241
+ function bootDaemon(helmDir) {
3242
+ const logPath = helmLogPath();
3243
+ mkdirSync2(dirname3(logPath), { recursive: true });
3244
+ const logFd = openSync(logPath, "a");
3245
+ const proc = Bun.spawn([resolveBun(), "src/server.ts"], {
3246
+ cwd: helmDir,
3247
+ stderr: logFd,
3248
+ stdin: "ignore",
3249
+ stdout: logFd
3250
+ });
3251
+ proc.unref();
3252
+ }
3253
+ async function waitForHealth(timeoutMs) {
3254
+ const deadline = Date.now() + timeoutMs;
3255
+ while (Date.now() < deadline) {
3256
+ if (await daemonHealthy()) {
3257
+ return true;
3258
+ }
3259
+ await Bun.sleep(250);
3260
+ }
3261
+ return false;
3262
+ }
3263
+ async function openAppWindow() {
3264
+ if (process.platform === "darwin" && chromeInstalled()) {
3265
+ const proc = Bun.spawn(["open", "-na", "Google Chrome", "--args", `--app=${HELM_URL}`], {
3266
+ stderr: "ignore",
3267
+ stdin: "ignore",
3268
+ stdout: "ignore"
3269
+ });
3270
+ if (await proc.exited === 0) {
3271
+ return;
3272
+ }
3273
+ }
3274
+ await openExternal(HELM_URL);
3275
+ }
3276
+ function chromeInstalled() {
3277
+ return existsSync("/Applications/Google Chrome.app") || existsSync(join4(homedir4(), "Applications/Google Chrome.app"));
3278
+ }
3279
+ async function helmOpenCommand() {
3280
+ if (await daemonHealthy()) {
3281
+ console.log(`The helm holds on :${HELM_PORT}. Opening the window.`);
3282
+ await openAppWindow();
3283
+ return;
3284
+ }
3285
+ const helmDir = resolveHelmDir();
3286
+ await ensureBuilt(helmDir);
3287
+ bootDaemon(helmDir);
3288
+ if (!await waitForHealth(15000)) {
3289
+ throw new CliError2("helm_no_answer", `The daemon didn't answer on :${HELM_PORT}. Its log is at ${helmLogPath()}.`);
3290
+ }
3291
+ console.log(`Helm raised on :${HELM_PORT}. Opening the window.`);
3292
+ await openAppWindow();
3293
+ }
3294
+ function xmlEscape(text) {
3295
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
3296
+ }
3297
+ function buildLaunchAgentPlist(helmDir, bunPath, logPath) {
3298
+ const path2 = `${dirname3(bunPath)}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`;
3299
+ return `<?xml version="1.0" encoding="UTF-8"?>
3300
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3301
+ <plist version="1.0">
3302
+ <dict>
3303
+ <key>EnvironmentVariables</key>
3304
+ <dict>
3305
+ <key>PATH</key>
3306
+ <string>${xmlEscape(path2)}</string>
3307
+ </dict>
3308
+ <key>KeepAlive</key>
3309
+ <dict>
3310
+ <key>SuccessfulExit</key>
3311
+ <false/>
3312
+ </dict>
3313
+ <key>Label</key>
3314
+ <string>${LAUNCH_AGENT_LABEL}</string>
3315
+ <key>ProgramArguments</key>
3316
+ <array>
3317
+ <string>${xmlEscape(bunPath)}</string>
3318
+ <string>src/server.ts</string>
3319
+ </array>
3320
+ <key>RunAtLoad</key>
3321
+ <true/>
3322
+ <key>StandardErrorPath</key>
3323
+ <string>${xmlEscape(logPath)}</string>
3324
+ <key>StandardOutPath</key>
3325
+ <string>${xmlEscape(logPath)}</string>
3326
+ <key>WorkingDirectory</key>
3327
+ <string>${xmlEscape(helmDir)}</string>
3328
+ </dict>
3329
+ </plist>
3330
+ `;
3331
+ }
3332
+ function currentUid() {
3333
+ const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
3334
+ if (uid === undefined) {
3335
+ throw new CliError2("no_uid", "Couldn't read the user id — launchd needs a gui domain.");
3336
+ }
3337
+ return uid;
3338
+ }
3339
+ async function launchctl(args) {
3340
+ const proc = Bun.spawn(["launchctl", ...args], {
3341
+ stderr: "ignore",
3342
+ stdin: "ignore",
3343
+ stdout: "ignore"
3344
+ });
3345
+ return proc.exited;
3346
+ }
3347
+ async function helmInstallCommand() {
3348
+ if (process.platform !== "darwin") {
3349
+ throw new CliError2("unsupported_platform", "launchd is a macOS thing. Nothing to install here.");
3350
+ }
3351
+ const helmDir = resolveHelmDir();
3352
+ await ensureBuilt(helmDir);
3353
+ const plistPath = launchAgentPlistPath();
3354
+ mkdirSync2(dirname3(plistPath), { recursive: true });
3355
+ mkdirSync2(dirname3(helmLogPath()), { recursive: true });
3356
+ writeFileSync2(plistPath, buildLaunchAgentPlist(helmDir, resolveBun(), helmLogPath()));
3357
+ const uid = currentUid();
3358
+ await launchctl(["bootout", `gui/${uid}/${LAUNCH_AGENT_LABEL}`]);
3359
+ const exitCode = await launchctl(["bootstrap", `gui/${uid}`, plistPath]);
3360
+ if (exitCode !== 0) {
3361
+ throw new CliError2("launchctl_refused", `launchctl bootstrap refused (exit ${exitCode}). The plist is at ${plistPath}.`);
3362
+ }
3363
+ console.log(`LaunchAgent installed (${LAUNCH_AGENT_LABEL}).`);
3364
+ console.log(`The daemon rises at login and holds :${HELM_PORT}. Log: ${helmLogPath()}`);
3365
+ }
3366
+ async function helmUninstallCommand() {
3367
+ if (process.platform !== "darwin") {
3368
+ throw new CliError2("unsupported_platform", "launchd is a macOS thing. Nothing to remove here.");
3369
+ }
3370
+ const plistPath = launchAgentPlistPath();
3371
+ await launchctl(["bootout", `gui/${currentUid()}/${LAUNCH_AGENT_LABEL}`]);
3372
+ if (existsSync(plistPath)) {
3373
+ rmSync2(plistPath);
3374
+ console.log("LaunchAgent removed and the daemon stood down.");
3375
+ return;
3376
+ }
3377
+ console.log("No LaunchAgent on file. Nothing stood down.");
3378
+ }
3379
+ var HELM_PORT = 4190, HELM_URL, LAUNCH_AGENT_LABEL = "com.fluncle.helm", HELM_DIR_FILE;
3380
+ var init_helm = __esm(() => {
3381
+ init_open_external();
3382
+ init_output();
3383
+ HELM_URL = `http://127.0.0.1:${HELM_PORT}`;
3384
+ HELM_DIR_FILE = join4(homedir4(), ".config/fluncle/helm-dir");
3385
+ });
3386
+
3147
3387
  // src/commands/add.ts
3148
3388
  var exports_add = {};
3149
3389
  __export(exports_add, {
@@ -3675,9 +3915,9 @@ var init_mixtape_youtube2 = __esm(() => {
3675
3915
 
3676
3916
  // src/commands/mixtape-set-video.ts
3677
3917
  import { randomUUID } from "node:crypto";
3678
- import { existsSync, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
3918
+ import { existsSync as existsSync2, rmSync as rmSync3, statSync as statSync3 } from "node:fs";
3679
3919
  import { tmpdir } from "node:os";
3680
- import { join as join4 } from "node:path";
3920
+ import { join as join5 } from "node:path";
3681
3921
  function planMultipart(contentLength, partSize = DEFAULT_PART_SIZE) {
3682
3922
  if (!Number.isInteger(contentLength) || contentLength <= 0) {
3683
3923
  throw new CliError2("invalid_size", `The rendition size must be a positive integer (got ${contentLength})`);
@@ -3732,11 +3972,11 @@ function renditionFfmpegArgs(inputPath, outputPath) {
3732
3972
  ];
3733
3973
  }
3734
3974
  async function uploadRenditionMultipart(masterPath, presignPath, onProgress = () => {}) {
3735
- if (!existsSync(masterPath)) {
3975
+ if (!existsSync2(masterPath)) {
3736
3976
  throw new CliError2("file_not_found", `Set-video master not found: ${masterPath}`);
3737
3977
  }
3738
3978
  await assertFfmpeg();
3739
- const renditionPath = join4(tmpdir(), `fluncle-set-${randomUUID()}.mp4`);
3979
+ const renditionPath = join5(tmpdir(), `fluncle-set-${randomUUID()}.mp4`);
3740
3980
  onProgress("Set video: deriving the 1080p faststart rendition (ffmpeg)…");
3741
3981
  await deriveRendition(masterPath, renditionPath);
3742
3982
  try {
@@ -3765,7 +4005,7 @@ async function uploadRenditionMultipart(masterPath, presignPath, onProgress = ()
3765
4005
  }
3766
4006
  return { key: presign.key, url: `${FOUND_BASE2}/${presign.key}` };
3767
4007
  } finally {
3768
- rmSync2(renditionPath, { force: true });
4008
+ rmSync3(renditionPath, { force: true });
3769
4009
  }
3770
4010
  }
3771
4011
  async function putPart(url, path2, part, onProgress = () => {}) {
@@ -3782,7 +4022,7 @@ async function putPart(url, path2, part, onProgress = () => {}) {
3782
4022
  }
3783
4023
  const backoffMs = 500 * 2 ** (attempt - 1);
3784
4024
  onProgress(`Set video: part ${part.partNumber} dropped — retry ${attempt}/${MAX_PART_ATTEMPTS - 1} in ${backoffMs}ms…`);
3785
- await new Promise((resolve) => setTimeout(resolve, backoffMs));
4025
+ await new Promise((resolve2) => setTimeout(resolve2, backoffMs));
3786
4026
  }
3787
4027
  }
3788
4028
  }
@@ -3863,7 +4103,7 @@ __export(exports_recordings, {
3863
4103
  recordingDeleteCommand: () => recordingDeleteCommand,
3864
4104
  recordingCreateCommand: () => recordingCreateCommand
3865
4105
  });
3866
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
4106
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
3867
4107
  async function recordingGet(id) {
3868
4108
  const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
3869
4109
  return response.recording;
@@ -3932,7 +4172,7 @@ async function recordingCreateCommand(options = {}) {
3932
4172
  if (!options.video) {
3933
4173
  throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
3934
4174
  }
3935
- if (!existsSync2(options.video)) {
4175
+ if (!existsSync3(options.video)) {
3936
4176
  throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
3937
4177
  }
3938
4178
  const created = await adminApiPost("/api/admin/recordings", {
@@ -3968,11 +4208,11 @@ async function recordingUpdateCommand(id, options = {}) {
3968
4208
  body.parentId = options.parentId;
3969
4209
  }
3970
4210
  if (options.tracklistFile !== undefined) {
3971
- if (!existsSync2(options.tracklistFile)) {
4211
+ if (!existsSync3(options.tracklistFile)) {
3972
4212
  throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
3973
4213
  }
3974
4214
  try {
3975
- body.tracklistJson = JSON.parse(readFileSync2(options.tracklistFile, "utf8"));
4215
+ body.tracklistJson = JSON.parse(readFileSync3(options.tracklistFile, "utf8"));
3976
4216
  } catch {
3977
4217
  throw new CliError2("invalid_tracklist", `Tracklist file is not valid JSON: ${options.tracklistFile}`);
3978
4218
  }
@@ -4017,12 +4257,12 @@ async function recordingReplaceCuesCommand(id, options = {}) {
4017
4257
  if (!options.cuesFile) {
4018
4258
  throw new CliError2("missing_cues_file", "replace-cues needs a --cues-file <file> (the ordered cue array)");
4019
4259
  }
4020
- if (!existsSync2(options.cuesFile)) {
4260
+ if (!existsSync3(options.cuesFile)) {
4021
4261
  throw new CliError2("file_not_found", `Cues file not found: ${options.cuesFile}`);
4022
4262
  }
4023
4263
  let cues;
4024
4264
  try {
4025
- cues = JSON.parse(readFileSync2(options.cuesFile, "utf8"));
4265
+ cues = JSON.parse(readFileSync3(options.cuesFile, "utf8"));
4026
4266
  } catch {
4027
4267
  throw new CliError2("invalid_cues", `Cues file is not valid JSON: ${options.cuesFile}`);
4028
4268
  }
@@ -4057,9 +4297,9 @@ __export(exports_clips, {
4057
4297
  CLIP_AUDIO_BITRATE: () => CLIP_AUDIO_BITRATE
4058
4298
  });
4059
4299
  import { randomUUID as randomUUID2 } from "node:crypto";
4060
- import { rmSync as rmSync3, statSync as statSync4 } from "node:fs";
4300
+ import { rmSync as rmSync4, statSync as statSync4 } from "node:fs";
4061
4301
  import { tmpdir as tmpdir2 } from "node:os";
4062
- import { join as join5 } from "node:path";
4302
+ import { join as join6 } from "node:path";
4063
4303
  function clipFootageKey(clipId) {
4064
4304
  return `${clipId}/footage.mp4`;
4065
4305
  }
@@ -4142,7 +4382,7 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
4142
4382
  }
4143
4383
  const source = await resolveClipSource(clip);
4144
4384
  await assertFfmpeg2();
4145
- const outputPath = join5(tmpdir2(), `fluncle-clip-${randomUUID2()}.mp4`);
4385
+ const outputPath = join6(tmpdir2(), `fluncle-clip-${randomUUID2()}.mp4`);
4146
4386
  try {
4147
4387
  onProgress(`Clip ${clipId}: cutting [${clip.inMs}–${clip.outMs}ms]…`);
4148
4388
  await runClipCut({
@@ -4168,7 +4408,7 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
4168
4408
  url: `${FOUND_BASE3}/${presign.key}`
4169
4409
  };
4170
4410
  } finally {
4171
- rmSync3(outputPath, { force: true });
4411
+ rmSync4(outputPath, { force: true });
4172
4412
  }
4173
4413
  }
4174
4414
  async function putClip(url, contentType, path2) {
@@ -4226,7 +4466,7 @@ __export(exports_recordings2, {
4226
4466
  recordingDeleteCommand: () => recordingDeleteCommand2,
4227
4467
  recordingCreateCommand: () => recordingCreateCommand2
4228
4468
  });
4229
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
4469
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
4230
4470
  async function recordingGet2(id) {
4231
4471
  const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
4232
4472
  return response.recording;
@@ -4295,7 +4535,7 @@ async function recordingCreateCommand2(options = {}) {
4295
4535
  if (!options.video) {
4296
4536
  throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
4297
4537
  }
4298
- if (!existsSync3(options.video)) {
4538
+ if (!existsSync4(options.video)) {
4299
4539
  throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
4300
4540
  }
4301
4541
  const created = await adminApiPost("/api/admin/recordings", {
@@ -4331,11 +4571,11 @@ async function recordingUpdateCommand2(id, options = {}) {
4331
4571
  body.parentId = options.parentId;
4332
4572
  }
4333
4573
  if (options.tracklistFile !== undefined) {
4334
- if (!existsSync3(options.tracklistFile)) {
4574
+ if (!existsSync4(options.tracklistFile)) {
4335
4575
  throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
4336
4576
  }
4337
4577
  try {
4338
- body.tracklistJson = JSON.parse(readFileSync3(options.tracklistFile, "utf8"));
4578
+ body.tracklistJson = JSON.parse(readFileSync4(options.tracklistFile, "utf8"));
4339
4579
  } catch {
4340
4580
  throw new CliError2("invalid_tracklist", `Tracklist file is not valid JSON: ${options.tracklistFile}`);
4341
4581
  }
@@ -4380,12 +4620,12 @@ async function recordingReplaceCuesCommand2(id, options = {}) {
4380
4620
  if (!options.cuesFile) {
4381
4621
  throw new CliError2("missing_cues_file", "replace-cues needs a --cues-file <file> (the ordered cue array)");
4382
4622
  }
4383
- if (!existsSync3(options.cuesFile)) {
4623
+ if (!existsSync4(options.cuesFile)) {
4384
4624
  throw new CliError2("file_not_found", `Cues file not found: ${options.cuesFile}`);
4385
4625
  }
4386
4626
  let cues;
4387
4627
  try {
4388
- cues = JSON.parse(readFileSync3(options.cuesFile, "utf8"));
4628
+ cues = JSON.parse(readFileSync4(options.cuesFile, "utf8"));
4389
4629
  } catch {
4390
4630
  throw new CliError2("invalid_cues", `Cues file is not valid JSON: ${options.cuesFile}`);
4391
4631
  }
@@ -4411,7 +4651,7 @@ __export(exports_newsletter, {
4411
4651
  newsletterDraftCommand: () => newsletterDraftCommand,
4412
4652
  newsletterDeleteCommand: () => newsletterDeleteCommand
4413
4653
  });
4414
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
4654
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
4415
4655
  function buildBody2(options, { requireContent }) {
4416
4656
  const body = {};
4417
4657
  if (options.contentFile !== undefined) {
@@ -4431,10 +4671,10 @@ function buildBody2(options, { requireContent }) {
4431
4671
  return body;
4432
4672
  }
4433
4673
  function readContentFile(filePath) {
4434
- if (!existsSync4(filePath)) {
4674
+ if (!existsSync5(filePath)) {
4435
4675
  throw new CliError2("file_not_found", `Content file not found: ${filePath}`);
4436
4676
  }
4437
- const text = readFileSync4(filePath, "utf-8");
4677
+ const text = readFileSync5(filePath, "utf-8");
4438
4678
  try {
4439
4679
  return JSON.parse(text);
4440
4680
  } catch (error) {
@@ -4823,7 +5063,7 @@ async function selectWithKeyboard2(items, options) {
4823
5063
  let renderedLines = 0;
4824
5064
  let done = false;
4825
5065
  const wasRaw = stdin.isRaw === true;
4826
- return await new Promise((resolve) => {
5066
+ return await new Promise((resolve2) => {
4827
5067
  function cleanup() {
4828
5068
  if (done) {
4829
5069
  return;
@@ -4838,14 +5078,14 @@ async function selectWithKeyboard2(items, options) {
4838
5078
  cleanup();
4839
5079
  stdout.write(renderedLines > 0 ? `
4840
5080
  ` : "");
4841
- resolve(item);
5081
+ resolve2(item);
4842
5082
  }
4843
5083
  function cancel() {
4844
5084
  cleanup();
4845
5085
  clearRendered2(stdout, renderedLines);
4846
5086
  stdout.write(`Cancelled.
4847
5087
  `);
4848
- resolve(undefined);
5088
+ resolve2(undefined);
4849
5089
  }
4850
5090
  function render() {
4851
5091
  clearRendered2(stdout, renderedLines);
@@ -4901,7 +5141,7 @@ async function paginateWithKeyboard(options) {
4901
5141
  let busy = false;
4902
5142
  let done = false;
4903
5143
  const wasRaw = stdin.isRaw === true;
4904
- return await new Promise((resolve) => {
5144
+ return await new Promise((resolve2) => {
4905
5145
  function cleanup() {
4906
5146
  if (done) {
4907
5147
  return;
@@ -4916,7 +5156,7 @@ async function paginateWithKeyboard(options) {
4916
5156
  cleanup();
4917
5157
  stdout.write(`
4918
5158
  `);
4919
- resolve();
5159
+ resolve2();
4920
5160
  }
4921
5161
  function render() {
4922
5162
  clearRendered2(stdout, renderedLines);
@@ -5069,7 +5309,7 @@ var init_format2 = __esm(() => {
5069
5309
  });
5070
5310
 
5071
5311
  // src/cli.ts
5072
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
5312
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
5073
5313
  import path2 from "path";
5074
5314
 
5075
5315
  // ../../node_modules/commander/lib/error.js
@@ -7215,6 +7455,7 @@ ${fluncleAsciiLogo}
7215
7455
  addMetaCommands(program2);
7216
7456
  addTrackCommands(program2);
7217
7457
  addAdminCommands(program2);
7458
+ addHelmCommands(program2);
7218
7459
  return program2;
7219
7460
  }
7220
7461
  async function main(args = process.argv.slice(2)) {
@@ -7313,6 +7554,21 @@ function addTrackCommands(program2) {
7313
7554
  await runTrackGet(idOrLogId, options, trackGetCommand2);
7314
7555
  });
7315
7556
  }
7557
+ function addHelmCommands(program2) {
7558
+ const helm = configureCommand(program2.command("helm", { hidden: true }).description("Fluncle's Helm \u2014 the operator's mission control"));
7559
+ helm.action(async () => {
7560
+ const { helmOpenCommand: helmOpenCommand2 } = await Promise.resolve().then(() => (init_helm(), exports_helm));
7561
+ await helmOpenCommand2();
7562
+ });
7563
+ helm.command("install").description("Install the launchd LaunchAgent (the daemon rises at login)").action(async () => {
7564
+ const { helmInstallCommand: helmInstallCommand2 } = await Promise.resolve().then(() => (init_helm(), exports_helm));
7565
+ await helmInstallCommand2();
7566
+ });
7567
+ helm.command("uninstall").description("Remove the LaunchAgent and stand the daemon down").action(async () => {
7568
+ const { helmUninstallCommand: helmUninstallCommand2 } = await Promise.resolve().then(() => (init_helm(), exports_helm));
7569
+ await helmUninstallCommand2();
7570
+ });
7571
+ }
7316
7572
  function addAdminCommands(program2) {
7317
7573
  const admin = configureCommand(program2.command("admin", { hidden: true }).description("Operator commands"));
7318
7574
  admin.action(() => {
@@ -7590,7 +7846,7 @@ async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCo
7590
7846
  console.log(` mime: ${result.mime}`);
7591
7847
  }
7592
7848
  async function runTrackObserve(idOrLogId, options, trackObserveCommand2) {
7593
- const script = options.scriptFile ? readFileSync5(options.scriptFile, "utf8") : options.script;
7849
+ const script = options.scriptFile ? readFileSync6(options.scriptFile, "utf8") : options.script;
7594
7850
  if (!idOrLogId || !script || !script.trim()) {
7595
7851
  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]");
7596
7852
  }
@@ -7646,7 +7902,7 @@ async function runTrackContext(idOrLogId, options, trackContextCommand2) {
7646
7902
  }
7647
7903
  }
7648
7904
  async function runTrackNote(idOrLogId, options, trackNoteCommand2) {
7649
- const note = options.scriptFile ? readFileSync5(options.scriptFile, "utf8") : options.script;
7905
+ const note = options.scriptFile ? readFileSync6(options.scriptFile, "utf8") : options.script;
7650
7906
  if (!idOrLogId || !note || !note.trim()) {
7651
7907
  throw new Error("Usage: fluncle admin tracks note <track_id|log_id> (--script <text> | --script-file <file>) [--json]");
7652
7908
  }
@@ -7792,7 +8048,7 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
7792
8048
  return;
7793
8049
  }
7794
8050
  const candidate = path2.join(dir, name);
7795
- return existsSync5(candidate) ? candidate : undefined;
8051
+ return existsSync6(candidate) ? candidate : undefined;
7796
8052
  };
7797
8053
  const resolveFile = (explicit, name) => {
7798
8054
  if (explicit) {
package/package.json CHANGED
@@ -31,5 +31,5 @@
31
31
  "url": "git+https://github.com/mauricekleine/fluncle.git"
32
32
  },
33
33
  "type": "module",
34
- "version": "0.93.0"
34
+ "version": "0.95.0"
35
35
  }