@writepanda/cli 1.221.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.
- package/bin/pandastudio.mjs +149 -39
- package/package.json +1 -1
package/bin/pandastudio.mjs
CHANGED
|
@@ -40,7 +40,7 @@ for (const stream of [process.stdout, process.stderr]) {
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
const VERSION = "1.
|
|
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
|
-
|
|
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 {
|
|
@@ -517,32 +559,80 @@ async function autoLaunchAndWait(opts, log) {
|
|
|
517
559
|
detached: true,
|
|
518
560
|
stdio: "ignore",
|
|
519
561
|
env: appLaunchEnv(process.env),
|
|
562
|
+
// The desktop app's own window must show (windowsHide hides a GUI
|
|
563
|
+
// program's first window too).
|
|
564
|
+
windowsHide: false,
|
|
565
|
+
});
|
|
566
|
+
child.on("error", () => {
|
|
567
|
+
/* reported by the readiness wait */
|
|
520
568
|
});
|
|
521
569
|
child.unref();
|
|
522
570
|
} catch (err) {
|
|
523
571
|
throw new Error(`failed to launch PandaStudio (${bin}): ${err?.message ?? err}`);
|
|
524
572
|
}
|
|
573
|
+
}
|
|
525
574
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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;
|
|
531
585
|
try {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
log(
|
|
535
|
-
`ready after ${Math.round((Date.now() - (deadline - opts.timeoutSeconds * 1000)) / 1000)}s`,
|
|
536
|
-
);
|
|
537
|
-
return credsKnown;
|
|
538
|
-
}
|
|
586
|
+
creds = await readCredentials(opts);
|
|
587
|
+
state = await probeHealthState(creds.port, 1500);
|
|
539
588
|
} catch {
|
|
540
589
|
/* token/port not written yet */
|
|
541
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;
|
|
542
616
|
await new Promise((r) => setTimeout(r, 500));
|
|
543
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
|
+
}
|
|
544
634
|
throw new Error(
|
|
545
|
-
`PandaStudio did not become ready within ${
|
|
635
|
+
`PandaStudio did not become ready within ${secs}s. Open PandaStudio, go to Settings → Automation and turn on Local automation.`,
|
|
546
636
|
);
|
|
547
637
|
}
|
|
548
638
|
|
|
@@ -702,14 +792,34 @@ async function main() {
|
|
|
702
792
|
// into the `args` object — the value `--slots='{"a":1}'` becomes
|
|
703
793
|
// `args.slots = {a:1}` thanks to parseScalar.
|
|
704
794
|
const callBody = { command, args: resolved.args };
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
795
|
+
const send = (c) =>
|
|
796
|
+
fetchJson(c.port, "/v1/call", {
|
|
797
|
+
method: "POST",
|
|
798
|
+
headers: {
|
|
799
|
+
"Content-Type": "application/json",
|
|
800
|
+
Authorization: `Bearer ${c.token}`,
|
|
801
|
+
},
|
|
802
|
+
body: JSON.stringify(callBody),
|
|
803
|
+
});
|
|
804
|
+
let result;
|
|
805
|
+
try {
|
|
806
|
+
result = await send(creds);
|
|
807
|
+
} catch (err) {
|
|
808
|
+
// Refused: the app moved its automation server after we read the
|
|
809
|
+
// port (nothing reached it, so resending can't run the command
|
|
810
|
+
// twice). Re-read the files once and resend to the new address.
|
|
811
|
+
const code = err?.code ?? err?.cause?.code;
|
|
812
|
+
if (code !== "ECONNREFUSED" || opts.portOverride) throw err;
|
|
813
|
+
let fresh;
|
|
814
|
+
try {
|
|
815
|
+
fresh = await readCredentials(opts);
|
|
816
|
+
} catch {
|
|
817
|
+
throw err;
|
|
818
|
+
}
|
|
819
|
+
if (fresh.port === creds.port && fresh.token === creds.token) throw err;
|
|
820
|
+
creds = fresh;
|
|
821
|
+
result = await send(creds);
|
|
822
|
+
}
|
|
713
823
|
|
|
714
824
|
return emit(result, opts);
|
|
715
825
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@writepanda/cli",
|
|
3
|
-
"version": "1.
|
|
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",
|