@writepanda/cli 1.222.0 → 1.224.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/pandastudio.mjs +118 -31
  2. package/package.json +1 -1
@@ -40,7 +40,7 @@ for (const stream of [process.stdout, process.stderr]) {
40
40
  });
41
41
  }
42
42
 
43
- const VERSION = "1.222.0";
43
+ const VERSION = "1.224.0";
44
44
 
45
45
  function configDir() {
46
46
  // PANDASTUDIO_CONFIG_DIR mirrors the override the main process
@@ -69,6 +69,10 @@ USAGE
69
69
  pandastudio transcript.transcribe --id=ID --force=true --language=tamil --provider=deepgram
70
70
  pandastudio transcript.detect-language --id=ID --clipId=CLIP
71
71
  pandastudio --version Print CLI version
72
+ pandastudio brand.list-assets --json List workspace brand media
73
+ pandastudio brand.add-asset --path=/absolute/logo.png --role=logo
74
+ pandastudio project.add-brand-asset --id=<project> --assetId=<asset> --atMs=0
75
+ pandastudio brand.remove-asset --id=<asset>
72
76
  pandastudio commands List every available verb.noun
73
77
 
74
78
  OPTIONS
@@ -389,22 +393,6 @@ async function fetchJson(port, pathSuffix, opts = {}) {
389
393
  return { status: res.status, body: parsed };
390
394
  }
391
395
 
392
- async function probeHealth(port, timeoutMs) {
393
- // Single short probe with a small AbortController; used as a
394
- // liveness check before auto-launching, then in a poll loop after
395
- // launch to wait for readiness.
396
- const ctrl = new AbortController();
397
- const t = setTimeout(() => ctrl.abort(), timeoutMs);
398
- try {
399
- const res = await fetch(`http://127.0.0.1:${port}/v1/health`, { signal: ctrl.signal });
400
- clearTimeout(t);
401
- return res.ok;
402
- } catch {
403
- clearTimeout(t);
404
- return false;
405
- }
406
- }
407
-
408
396
  /**
409
397
  * One health probe: "up" (answered), "busy" (the socket is open but no answer
410
398
  * within `timeoutMs`: the app's main process is stalled, e.g. creating an
@@ -509,7 +497,61 @@ function appLaunchEnv(env) {
509
497
  return out;
510
498
  }
511
499
 
512
- async function autoLaunchAndWait(opts, log) {
500
+ // Launch guard (mirrors ensureApp in packages/mcp/bin/appConnection.mjs; this
501
+ // script ships as one standalone file). Launching whenever the port file's
502
+ // server didn't answer started a NEW PandaStudio while one was busy, booting,
503
+ // or had its token/port files replaced (Jared, Windows 2.1.15: "it restarted
504
+ // with every request"). Launch only when nothing says PandaStudio is running,
505
+ // and at most once per LAUNCH_DEBOUNCE_MS from any process on the machine.
506
+ const DEFAULT_AUTOMATION_PORT = 7878;
507
+ const INSTANCE_STALE_MS = 20_000;
508
+ const LAUNCH_DEBOUNCE_MS = 90_000;
509
+
510
+ function pidAlive(pid) {
511
+ if (!Number.isInteger(pid) || pid <= 0) return false;
512
+ try {
513
+ process.kill(pid, 0);
514
+ return true;
515
+ } catch (err) {
516
+ return err?.code === "EPERM";
517
+ }
518
+ }
519
+
520
+ /** instance.json names a live PandaStudio with a fresh heartbeat. */
521
+ async function appInstanceAlive() {
522
+ try {
523
+ const raw = await fs.readFile(path.join(configDir(), "instance.json"), "utf-8");
524
+ const rec = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw);
525
+ const beat = Number(rec?.heartbeatAt);
526
+ if (!Number.isFinite(beat) || Date.now() - beat > INSTANCE_STALE_MS) return false;
527
+ return pidAlive(rec.pid);
528
+ } catch {
529
+ return false;
530
+ }
531
+ }
532
+
533
+ async function lastLaunchAgeMs() {
534
+ try {
535
+ const at = Number(
536
+ (await fs.readFile(path.join(configDir(), "launch-requested"), "utf-8")).trim(),
537
+ );
538
+ if (!Number.isFinite(at) || at > Date.now()) return null;
539
+ return Date.now() - at;
540
+ } catch {
541
+ return null;
542
+ }
543
+ }
544
+
545
+ async function recordLaunch() {
546
+ try {
547
+ await fs.mkdir(configDir(), { recursive: true });
548
+ await fs.writeFile(path.join(configDir(), "launch-requested"), String(Date.now()), "utf-8");
549
+ } catch {
550
+ /* best-effort */
551
+ }
552
+ }
553
+
554
+ function launchApp(opts, log) {
513
555
  const bin = findInstalledApp(opts.appOverride);
514
556
  log(`auto-launching ${bin}`);
515
557
  try {
@@ -521,31 +563,76 @@ async function autoLaunchAndWait(opts, log) {
521
563
  // program's first window too).
522
564
  windowsHide: false,
523
565
  });
566
+ child.on("error", () => {
567
+ /* reported by the readiness wait */
568
+ });
524
569
  child.unref();
525
570
  } catch (err) {
526
571
  throw new Error(`failed to launch PandaStudio (${bin}): ${err?.message ?? err}`);
527
572
  }
573
+ }
528
574
 
529
- const deadline = Date.now() + opts.timeoutSeconds * 1000;
530
- let credsKnown = null;
531
- while (Date.now() < deadline) {
532
- // Re-read credentials each iteration — the app writes them only
533
- // after binding, so the file may not exist yet.
575
+ async function autoLaunchAndWait(opts, log) {
576
+ const started = Date.now();
577
+ const deadline = started + opts.timeoutSeconds * 1000;
578
+ let launched = false;
579
+ let last = { state: null, alive: false, preferred: null };
580
+ for (;;) {
581
+ // Re-read credentials each round: the app writes them only after
582
+ // binding, and its heartbeat rewrites them if they drifted.
583
+ let creds = null;
584
+ let state = null;
534
585
  try {
535
- credsKnown = await readCredentials(opts);
536
- if (await probeHealth(credsKnown.port, 2000)) {
537
- log(
538
- `ready after ${Math.round((Date.now() - (deadline - opts.timeoutSeconds * 1000)) / 1000)}s`,
539
- );
540
- return credsKnown;
541
- }
586
+ creds = await readCredentials(opts);
587
+ state = await probeHealthState(creds.port, 1500);
542
588
  } catch {
543
589
  /* token/port not written yet */
544
590
  }
591
+ if (state === "up") {
592
+ if (launched) log(`ready after ${Math.round((Date.now() - started) / 1000)}s`);
593
+ return creds;
594
+ }
595
+ const alive = await appInstanceAlive();
596
+ let preferred = null;
597
+ if (!alive && state !== "busy" && creds?.port !== DEFAULT_AUTOMATION_PORT) {
598
+ preferred = await probeHealthState(DEFAULT_AUTOMATION_PORT, 1000);
599
+ }
600
+ last = { state, alive, preferred };
601
+ const age = launched ? 0 : await lastLaunchAgeMs();
602
+ const mayLaunch =
603
+ state !== "busy" &&
604
+ !alive &&
605
+ preferred !== "up" &&
606
+ preferred !== "busy" &&
607
+ (age === null || age >= LAUNCH_DEBOUNCE_MS);
608
+ if (mayLaunch) {
609
+ await recordLaunch();
610
+ launched = true;
611
+ launchApp(opts, log);
612
+ } else if (!launched && Date.now() - started < 600) {
613
+ log("PandaStudio is already running; waiting for it to answer (not launching another)");
614
+ }
615
+ if (Date.now() >= deadline) break;
545
616
  await new Promise((r) => setTimeout(r, 500));
546
617
  }
618
+ const secs = opts.timeoutSeconds;
619
+ if (last.state === "busy") {
620
+ throw new Error(
621
+ `PandaStudio is running but busy: it didn't answer for ${secs}s. Try again in a moment; nothing was relaunched.`,
622
+ );
623
+ }
624
+ if (last.alive) {
625
+ throw new Error(
626
+ `PandaStudio is running but its automation server didn't answer within ${secs}s. In PandaStudio open Settings → Automation and make sure Local automation is on.`,
627
+ );
628
+ }
629
+ if (last.preferred === "up") {
630
+ throw new Error(
631
+ `A PandaStudio is answering on 127.0.0.1:${DEFAULT_AUTOMATION_PORT} but its access files in ${configDir()} are missing or stale. Quit PandaStudio completely and open it again.`,
632
+ );
633
+ }
547
634
  throw new Error(
548
- `PandaStudio did not become ready within ${opts.timeoutSeconds}s. Open PandaStudio, go to Settings → Automation and turn on Local automation.`,
635
+ `PandaStudio did not become ready within ${secs}s. Open PandaStudio, go to Settings → Automation and turn on Local automation.`,
549
636
  );
550
637
  }
551
638
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writepanda/cli",
3
- "version": "1.222.0",
3
+ "version": "1.224.0",
4
4
  "description": "Drive PandaStudio (a desktop video editor for YouTube creators) from the command line. Talks to a localhost HTTP API the desktop app exposes.",
5
5
  "keywords": [
6
6
  "pandastudio",