@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.
- package/bin/pandastudio.mjs +118 -31
- 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 {
|
|
@@ -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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
536
|
-
|
|
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 ${
|
|
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.
|
|
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",
|