@writepanda/cli 1.122.0 → 1.183.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 +344 -34
- package/package.json +1 -1
package/bin/pandastudio.mjs
CHANGED
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
// %ProgramFiles%/PandaStudio/cli/pandastudio.exe (Windows installer)
|
|
22
22
|
|
|
23
23
|
import { spawn } from "node:child_process";
|
|
24
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
24
25
|
import fs from "node:fs/promises";
|
|
26
|
+
import http from "node:http";
|
|
25
27
|
import os from "node:os";
|
|
26
28
|
import path from "node:path";
|
|
27
29
|
import process from "node:process";
|
|
@@ -74,6 +76,12 @@ OPTIONS
|
|
|
74
76
|
--token=<value> Override the on-disk token (rare; for tests).
|
|
75
77
|
--port=<n> Override the on-disk port (rare; for tests).
|
|
76
78
|
--app=<path> Override the path to the PandaStudio binary used for auto-launch.
|
|
79
|
+
--key=@file.json Read one argument's value from a file (JSON, else text).
|
|
80
|
+
--args-file=<path> Read all arguments as one JSON object from a file
|
|
81
|
+
(also: pandastudio <command> @args.json; - = stdin).
|
|
82
|
+
--args-stdin Read all arguments as one JSON object from stdin.
|
|
83
|
+
--dry-run Print the command + arguments that would be sent; don't call the app.
|
|
84
|
+
--help-windows How to pass JSON arguments from cmd.exe / PowerShell / Git Bash.
|
|
77
85
|
|
|
78
86
|
EXAMPLES
|
|
79
87
|
pandastudio system.status
|
|
@@ -89,10 +97,23 @@ DOCS
|
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
function parseArgv(argv) {
|
|
92
|
-
const args = { _: [], flags: {}, json: false, noLaunch: false, timeoutSeconds: 60 };
|
|
93
|
-
for (
|
|
100
|
+
const args = { _: [], flags: {}, rawFlags: {}, json: false, noLaunch: false, timeoutSeconds: 60 };
|
|
101
|
+
for (let i = 0; i < argv.length; i++) {
|
|
102
|
+
const raw = argv[i];
|
|
94
103
|
if (raw === "--help" || raw === "-h") {
|
|
95
104
|
args.help = true;
|
|
105
|
+
} else if (raw === "--help-windows") {
|
|
106
|
+
args.helpWindows = true;
|
|
107
|
+
} else if (raw === "--dry-run") {
|
|
108
|
+
args.dryRun = true;
|
|
109
|
+
} else if (raw === "--args-stdin") {
|
|
110
|
+
args.argsStdin = true;
|
|
111
|
+
} else if (raw.startsWith("--args-file=")) {
|
|
112
|
+
args.argsFile = raw.slice("--args-file=".length);
|
|
113
|
+
} else if (raw === "--args-file") {
|
|
114
|
+
// The one flag that also takes the `--flag value` form: a path is
|
|
115
|
+
// easy to mistype that way and the error would be baffling.
|
|
116
|
+
args.argsFile = argv[++i] ?? "";
|
|
96
117
|
} else if (raw === "--version" || raw === "-v") {
|
|
97
118
|
args.version = true;
|
|
98
119
|
} else if (raw === "--json") {
|
|
@@ -116,8 +137,17 @@ function parseArgv(argv) {
|
|
|
116
137
|
} else {
|
|
117
138
|
const key = raw.slice(2, eq);
|
|
118
139
|
const value = raw.slice(eq + 1);
|
|
140
|
+
args.rawFlags[key] = value;
|
|
119
141
|
args.flags[key] = parseScalar(value);
|
|
120
142
|
}
|
|
143
|
+
} else if (
|
|
144
|
+
args._.length > 0 &&
|
|
145
|
+
raw.length > 1 &&
|
|
146
|
+
raw.startsWith("@") &&
|
|
147
|
+
!raw.startsWith("@@")
|
|
148
|
+
) {
|
|
149
|
+
// `pandastudio project.add-zoom @args.json` = --args-file=args.json
|
|
150
|
+
args.argsFile = raw.slice(1);
|
|
121
151
|
} else {
|
|
122
152
|
args._.push(raw);
|
|
123
153
|
}
|
|
@@ -125,6 +155,172 @@ function parseArgv(argv) {
|
|
|
125
155
|
return args;
|
|
126
156
|
}
|
|
127
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Detects JSON a Windows shell mangled on the way in, returning a short
|
|
160
|
+
* reason or null. Two shapes:
|
|
161
|
+
* - quotes stripped (cmd.exe, Windows PowerShell 5.1, Git Bash calling a
|
|
162
|
+
* .cmd): `[{"timeMs":0,"x":"a"}]` arrives as `[{timeMs:0,x:a}]`;
|
|
163
|
+
* - quotes left escaped (PowerShell 7.3+ given the 5.1 recipe):
|
|
164
|
+
* `[{\"timeMs\":0}]`.
|
|
165
|
+
* Only text that becomes valid JSON once repaired counts, so an ordinary
|
|
166
|
+
* string that happens to start with a bracket (`[Note: hi]`) passes through.
|
|
167
|
+
*/
|
|
168
|
+
export function detectMangledJson(value) {
|
|
169
|
+
if (typeof value !== "string") return null;
|
|
170
|
+
const t = value.trim();
|
|
171
|
+
if (!(t.startsWith("{") || t.startsWith("["))) return null;
|
|
172
|
+
try {
|
|
173
|
+
JSON.parse(t);
|
|
174
|
+
return null;
|
|
175
|
+
} catch {
|
|
176
|
+
/* not JSON as given; see whether a shell broke it */
|
|
177
|
+
}
|
|
178
|
+
if (t.includes('\\"')) {
|
|
179
|
+
try {
|
|
180
|
+
JSON.parse(t.replace(/\\"/g, '"'));
|
|
181
|
+
return 'its double quotes arrived escaped as \\"';
|
|
182
|
+
} catch {
|
|
183
|
+
/* not that either */
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Needs real structure (an object, or a list of several items) so a
|
|
187
|
+
// bracketed word like "[draft]" stays plain text.
|
|
188
|
+
const structured = (t.includes("{") && t.includes(":")) || (t.startsWith("[") && t.includes(","));
|
|
189
|
+
const repaired = structured ? requoteBareJson(t) : null;
|
|
190
|
+
if (repaired !== null) {
|
|
191
|
+
try {
|
|
192
|
+
JSON.parse(repaired);
|
|
193
|
+
return "its double quotes were removed by the shell";
|
|
194
|
+
} catch {
|
|
195
|
+
/* not JSON-shaped */
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Puts double quotes back around bare keys and bare string values
|
|
202
|
+
* (`{a:b c,d:[e,1]}` → `{"a":"b c","d":["e",1]}`); null when the text has
|
|
203
|
+
* a quote of its own (then it wasn't simply stripped). */
|
|
204
|
+
function requoteBareJson(t) {
|
|
205
|
+
if (t.includes('"')) return null;
|
|
206
|
+
const quoteToken = (tok) => {
|
|
207
|
+
const s = tok.trim();
|
|
208
|
+
if (s === "") return tok;
|
|
209
|
+
if (/^(true|false|null)$/.test(s) || /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(s)) return tok;
|
|
210
|
+
return tok.replace(s, JSON.stringify(s));
|
|
211
|
+
};
|
|
212
|
+
// Split on structural characters, keeping them, and quote everything in
|
|
213
|
+
// between that isn't a number/literal.
|
|
214
|
+
return t
|
|
215
|
+
.split(/([{}[\],:])/)
|
|
216
|
+
.map((part) => (/^[{}[\],:]$/.test(part) ? part : quoteToken(part)))
|
|
217
|
+
.join("");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const WINDOWS_QUOTING_HELP = `Windows shells change JSON arguments on the way in: cmd.exe and Windows
|
|
221
|
+
PowerShell 5.1 strip the double quotes, so
|
|
222
|
+
--keyframes=[{"timeMs":0}] arrives as [{timeMs:0}]. These work on every shell:
|
|
223
|
+
--keyframes=@keyframes.json read one value from a file (JSON, else text)
|
|
224
|
+
--args-file=args.json all arguments as one JSON object from a file
|
|
225
|
+
pandastudio <command> @args.json (same as --args-file)
|
|
226
|
+
--args-stdin all arguments as one JSON object on stdin,
|
|
227
|
+
e.g. Get-Content args.json | pandastudio <command> --args-stdin
|
|
228
|
+
Inline, if you must:
|
|
229
|
+
cmd.exe: "--keyframes=[{\\"timeMs\\":0}]"
|
|
230
|
+
Windows PowerShell 5.1: '--keyframes=[{\\"timeMs\\":0}]'
|
|
231
|
+
PowerShell 7.3+: '--keyframes=[{"timeMs":0}]'
|
|
232
|
+
Add --dry-run to print the exact arguments that would be sent, without calling the app.
|
|
233
|
+
A literal value starting with @ is written @@ (--text=@@handle sends "@handle").`;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The final args object: --args-file / --args-stdin first, then the
|
|
237
|
+
* command-line flags on top. A value `@path` is read from that file (JSON
|
|
238
|
+
* when it parses, else the text; `@@x` is the literal "@x"). Errors (a
|
|
239
|
+
* missing file, an args file that isn't an object, JSON a Windows shell
|
|
240
|
+
* mangled) come back as `error` so nothing half-parsed is ever sent.
|
|
241
|
+
*/
|
|
242
|
+
export async function resolveArgs(opts, { readStdin, cwd = process.cwd() } = {}) {
|
|
243
|
+
const out = {};
|
|
244
|
+
const readStdinText = () => (readStdin ? readStdin() : readAllStdin());
|
|
245
|
+
const readJsonObject = (text, where) => {
|
|
246
|
+
let v;
|
|
247
|
+
try {
|
|
248
|
+
v = JSON.parse(text.replace(/^/, ""));
|
|
249
|
+
} catch (e) {
|
|
250
|
+
const why = detectMangledJson(text);
|
|
251
|
+
throw new Error(
|
|
252
|
+
why
|
|
253
|
+
? `${where} is not valid JSON: ${why}.\n\n${WINDOWS_QUOTING_HELP}`
|
|
254
|
+
: `${where} is not valid JSON (${e.message}).`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`${where} must be a JSON object of arguments, e.g. {"id":"abc","atMs":1000}.`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return v;
|
|
263
|
+
};
|
|
264
|
+
try {
|
|
265
|
+
if (opts.argsFile === "") throw new Error("--args-file needs a path: --args-file=args.json");
|
|
266
|
+
if (opts.argsFile === "-") {
|
|
267
|
+
Object.assign(out, readJsonObject(await readStdinText(), "--args-file=- (stdin)"));
|
|
268
|
+
} else if (opts.argsFile) {
|
|
269
|
+
const p = path.resolve(cwd, opts.argsFile);
|
|
270
|
+
let text;
|
|
271
|
+
try {
|
|
272
|
+
text = await fs.readFile(p, "utf-8");
|
|
273
|
+
} catch {
|
|
274
|
+
throw new Error(`--args-file: can't read ${p}.`);
|
|
275
|
+
}
|
|
276
|
+
Object.assign(out, readJsonObject(text, `--args-file ${p}`));
|
|
277
|
+
}
|
|
278
|
+
if (opts.argsStdin) {
|
|
279
|
+
Object.assign(out, readJsonObject(await readStdinText(), "--args-stdin input"));
|
|
280
|
+
}
|
|
281
|
+
for (const [key, value] of Object.entries(opts.flags)) {
|
|
282
|
+
const raw = opts.rawFlags?.[key];
|
|
283
|
+
if (typeof raw === "string" && raw.startsWith("@@")) {
|
|
284
|
+
out[key] = raw.slice(1);
|
|
285
|
+
} else if (typeof raw === "string" && raw.length > 1 && raw.startsWith("@")) {
|
|
286
|
+
const p = path.resolve(cwd, raw.slice(1));
|
|
287
|
+
let text;
|
|
288
|
+
try {
|
|
289
|
+
text = await fs.readFile(p, "utf-8");
|
|
290
|
+
} catch {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`--${key}=@${raw.slice(1)}: can't read ${p}. (A literal value starting with @ is written @@.)`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const trimmed = text.replace(/^/, "").replace(/\r?\n$/, "");
|
|
296
|
+
try {
|
|
297
|
+
out[key] = JSON.parse(trimmed);
|
|
298
|
+
} catch {
|
|
299
|
+
const why = detectMangledJson(trimmed);
|
|
300
|
+
if (why)
|
|
301
|
+
throw new Error(`--${key}=@${raw.slice(1)}: the file is not valid JSON (${why}).`);
|
|
302
|
+
out[key] = trimmed;
|
|
303
|
+
}
|
|
304
|
+
} else if (typeof raw === "string" && detectMangledJson(raw)) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
`--${key}=${raw} looks like JSON that the shell changed: ${detectMangledJson(raw)}. Nothing was sent.\n\n${WINDOWS_QUOTING_HELP}`,
|
|
307
|
+
);
|
|
308
|
+
} else {
|
|
309
|
+
out[key] = value;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch (err) {
|
|
313
|
+
return { error: err?.message ?? String(err) };
|
|
314
|
+
}
|
|
315
|
+
return { args: out };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function readAllStdin() {
|
|
319
|
+
const chunks = [];
|
|
320
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
321
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
322
|
+
}
|
|
323
|
+
|
|
128
324
|
function parseScalar(value) {
|
|
129
325
|
// JSON pass-through for objects/arrays/booleans/numbers; fall back
|
|
130
326
|
// to the literal string. Lets `--slots='{"title":"x"}'` work without
|
|
@@ -155,13 +351,33 @@ async function readCredentials(opts) {
|
|
|
155
351
|
return { port, token };
|
|
156
352
|
}
|
|
157
353
|
|
|
354
|
+
// The app answers /v1/call when the command finishes, and some commands run
|
|
355
|
+
// for many minutes (an AI presenter take, a long music track). Node's global
|
|
356
|
+
// fetch gives up on a response after 5 minutes ("fetch failed"), so calls go
|
|
357
|
+
// through node:http, which waits as long as the command takes.
|
|
358
|
+
function httpRequestLocal(port, pathSuffix, { method = "GET", headers = {}, body } = {}) {
|
|
359
|
+
return new Promise((resolve, reject) => {
|
|
360
|
+
const req = http.request(
|
|
361
|
+
{ host: "127.0.0.1", port, path: pathSuffix, method, headers },
|
|
362
|
+
(res) => {
|
|
363
|
+
const chunks = [];
|
|
364
|
+
res.on("data", (c) => chunks.push(c));
|
|
365
|
+
res.on("end", () =>
|
|
366
|
+
resolve({ status: res.statusCode ?? 0, text: Buffer.concat(chunks).toString("utf-8") }),
|
|
367
|
+
);
|
|
368
|
+
res.on("error", reject);
|
|
369
|
+
},
|
|
370
|
+
);
|
|
371
|
+
req.on("error", reject);
|
|
372
|
+
req.setTimeout(0);
|
|
373
|
+
if (body !== undefined) req.write(body);
|
|
374
|
+
req.end();
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
158
378
|
async function fetchJson(port, pathSuffix, opts = {}) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// safe to assume.
|
|
162
|
-
const url = `http://127.0.0.1:${port}${pathSuffix}`;
|
|
163
|
-
const res = await fetch(url, opts);
|
|
164
|
-
const text = await res.text();
|
|
379
|
+
const res = await httpRequestLocal(port, pathSuffix, opts);
|
|
380
|
+
const text = res.text;
|
|
165
381
|
let parsed;
|
|
166
382
|
try {
|
|
167
383
|
parsed = text.length === 0 ? null : JSON.parse(text);
|
|
@@ -187,32 +403,108 @@ async function probeHealth(port, timeoutMs) {
|
|
|
187
403
|
}
|
|
188
404
|
}
|
|
189
405
|
|
|
406
|
+
/**
|
|
407
|
+
* One health probe: "up" (answered), "busy" (the socket is open but no answer
|
|
408
|
+
* within `timeoutMs`: the app's main process is stalled, e.g. creating an
|
|
409
|
+
* editor window on a loaded machine) or "down" (connection refused / reset:
|
|
410
|
+
* not running).
|
|
411
|
+
*/
|
|
412
|
+
async function probeHealthState(port, timeoutMs) {
|
|
413
|
+
const ctrl = new AbortController();
|
|
414
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
415
|
+
try {
|
|
416
|
+
const res = await fetch(`http://127.0.0.1:${port}/v1/health`, { signal: ctrl.signal });
|
|
417
|
+
return res.ok ? "up" : "down";
|
|
418
|
+
} catch {
|
|
419
|
+
return ctrl.signal.aborted ? "busy" : "down";
|
|
420
|
+
} finally {
|
|
421
|
+
clearTimeout(t);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Is the app reachable? A busy app is waited for (up to `busyWaitMs`) rather
|
|
427
|
+
* than reported as not running: a single 1.5 s probe used to fail whenever the
|
|
428
|
+
* main process stalled for longer (window creation takes ~0.4 s idle and
|
|
429
|
+
* several seconds under load), so agents saw "not reachable" at random.
|
|
430
|
+
*/
|
|
431
|
+
async function appReachable(port, { probeMs = 1500, busyWaitMs = 30_000, onBusy } = {}) {
|
|
432
|
+
const deadline = Date.now() + busyWaitMs;
|
|
433
|
+
let warned = false;
|
|
434
|
+
for (;;) {
|
|
435
|
+
const s = await probeHealthState(port, probeMs);
|
|
436
|
+
if (s === "up") return true;
|
|
437
|
+
if (s === "down" || Date.now() >= deadline) return false;
|
|
438
|
+
if (!warned && onBusy) onBusy();
|
|
439
|
+
warned = true;
|
|
440
|
+
probeMs = Math.min(probeMs * 2, 5000);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
190
444
|
function findInstalledApp(appOverride) {
|
|
191
445
|
if (appOverride) return appOverride;
|
|
192
446
|
if (process.env.PANDASTUDIO_BIN) return process.env.PANDASTUDIO_BIN;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
"PandaStudio.app",
|
|
201
|
-
"Contents",
|
|
202
|
-
"MacOS",
|
|
203
|
-
"PandaStudio",
|
|
204
|
-
),
|
|
205
|
-
];
|
|
206
|
-
return macCandidates[0]; // best-effort; spawn() will surface ENOENT clearly
|
|
447
|
+
// The app records its own location (it can live anywhere on Windows,
|
|
448
|
+
// and in ~/Applications on a Mac).
|
|
449
|
+
try {
|
|
450
|
+
const recorded = readFileSync(path.join(configDir(), "app-path"), "utf-8").trim();
|
|
451
|
+
if (recorded && existsSync(recorded)) return recorded;
|
|
452
|
+
} catch {
|
|
453
|
+
/* older app, or never launched */
|
|
207
454
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
455
|
+
const candidates =
|
|
456
|
+
process.platform === "darwin"
|
|
457
|
+
? [
|
|
458
|
+
"/Applications/PandaStudio.app/Contents/MacOS/PandaStudio",
|
|
459
|
+
path.join(
|
|
460
|
+
os.homedir(),
|
|
461
|
+
"Applications",
|
|
462
|
+
"PandaStudio.app",
|
|
463
|
+
"Contents",
|
|
464
|
+
"MacOS",
|
|
465
|
+
"PandaStudio",
|
|
466
|
+
),
|
|
467
|
+
]
|
|
468
|
+
: process.platform === "win32"
|
|
469
|
+
? [
|
|
470
|
+
// Per-user install (the installer default).
|
|
471
|
+
path.join(
|
|
472
|
+
process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local"),
|
|
473
|
+
"Programs",
|
|
474
|
+
"PandaStudio",
|
|
475
|
+
"PandaStudio.exe",
|
|
476
|
+
),
|
|
477
|
+
path.join(
|
|
478
|
+
process.env.ProgramFiles ?? "C:\\Program Files",
|
|
479
|
+
"PandaStudio",
|
|
480
|
+
"PandaStudio.exe",
|
|
481
|
+
),
|
|
482
|
+
]
|
|
483
|
+
: ["/opt/PandaStudio/pandastudio"];
|
|
484
|
+
// spawn() surfaces ENOENT clearly when none exists.
|
|
485
|
+
return candidates.find((c) => existsSync(c)) ?? candidates[0];
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Environment for launching the desktop app. The bundled launcher runs this
|
|
489
|
+
* CLI as Electron in node mode (ELECTRON_RUN_AS_NODE=1); inherited, that made
|
|
490
|
+
* "launch PandaStudio" start a Node REPL that exits at once, so auto-launch
|
|
491
|
+
* always ended in "did not become ready". Drop it (and the in-app markers). */
|
|
492
|
+
function appLaunchEnv(env) {
|
|
493
|
+
const out = {};
|
|
494
|
+
for (const [k, v] of Object.entries(env)) {
|
|
495
|
+
const upper = k.toUpperCase();
|
|
496
|
+
if (
|
|
497
|
+
upper === "ELECTRON_RUN_AS_NODE" ||
|
|
498
|
+
upper === "ELECTRON_NO_ATTACH_CONSOLE" ||
|
|
499
|
+
upper === "PANDASTUDIO_CALLER" ||
|
|
500
|
+
upper === "PANDASTUDIO_AUTOMATION_PORT" ||
|
|
501
|
+
upper === "PANDASTUDIO_AUTOMATION_TOKEN"
|
|
502
|
+
) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
out[k] = v;
|
|
211
506
|
}
|
|
212
|
-
|
|
213
|
-
// here means the day we publish a .AppImage or .deb the auto-launch
|
|
214
|
-
// path is one PR away.
|
|
215
|
-
return "/opt/PandaStudio/pandastudio";
|
|
507
|
+
return out;
|
|
216
508
|
}
|
|
217
509
|
|
|
218
510
|
async function autoLaunchAndWait(opts, log) {
|
|
@@ -222,7 +514,7 @@ async function autoLaunchAndWait(opts, log) {
|
|
|
222
514
|
const child = spawn(bin, [], {
|
|
223
515
|
detached: true,
|
|
224
516
|
stdio: "ignore",
|
|
225
|
-
env: process.env,
|
|
517
|
+
env: appLaunchEnv(process.env),
|
|
226
518
|
});
|
|
227
519
|
child.unref();
|
|
228
520
|
} catch (err) {
|
|
@@ -248,7 +540,7 @@ async function autoLaunchAndWait(opts, log) {
|
|
|
248
540
|
await new Promise((r) => setTimeout(r, 500));
|
|
249
541
|
}
|
|
250
542
|
throw new Error(
|
|
251
|
-
`PandaStudio did not become ready within ${opts.timeoutSeconds}s. Open
|
|
543
|
+
`PandaStudio did not become ready within ${opts.timeoutSeconds}s. Open PandaStudio, go to Settings → Automation and turn on Local automation.`,
|
|
252
544
|
);
|
|
253
545
|
}
|
|
254
546
|
|
|
@@ -337,6 +629,10 @@ async function main() {
|
|
|
337
629
|
process.stdout.write(`${VERSION}\n`);
|
|
338
630
|
return 0;
|
|
339
631
|
}
|
|
632
|
+
if (opts.helpWindows) {
|
|
633
|
+
process.stdout.write(`${WINDOWS_QUOTING_HELP}\n`);
|
|
634
|
+
return 0;
|
|
635
|
+
}
|
|
340
636
|
if (opts.help || (opts._.length === 0 && Object.keys(opts.flags).length === 0)) {
|
|
341
637
|
printHelp();
|
|
342
638
|
return 0;
|
|
@@ -362,6 +658,16 @@ async function main() {
|
|
|
362
658
|
return 2;
|
|
363
659
|
}
|
|
364
660
|
|
|
661
|
+
const resolved = await resolveArgs(opts);
|
|
662
|
+
if (resolved.error) {
|
|
663
|
+
process.stderr.write(`error: ${resolved.error}\n`);
|
|
664
|
+
return 2;
|
|
665
|
+
}
|
|
666
|
+
if (opts.dryRun) {
|
|
667
|
+
process.stdout.write(`${JSON.stringify({ command, args: resolved.args }, null, 2)}\n`);
|
|
668
|
+
return 0;
|
|
669
|
+
}
|
|
670
|
+
|
|
365
671
|
let creds;
|
|
366
672
|
try {
|
|
367
673
|
creds = await readCredentials(opts);
|
|
@@ -369,11 +675,15 @@ async function main() {
|
|
|
369
675
|
creds = null;
|
|
370
676
|
}
|
|
371
677
|
|
|
372
|
-
let alive = creds
|
|
678
|
+
let alive = creds
|
|
679
|
+
? await appReachable(creds.port, {
|
|
680
|
+
onBusy: () => log("PandaStudio is busy; waiting for it to answer"),
|
|
681
|
+
})
|
|
682
|
+
: false;
|
|
373
683
|
if (!alive) {
|
|
374
684
|
if (opts.noLaunch) {
|
|
375
685
|
process.stderr.write(
|
|
376
|
-
"error: PandaStudio is not reachable and --no-launch was set.\n Open
|
|
686
|
+
"error: PandaStudio is not reachable and --no-launch was set.\n Open PandaStudio, go to Settings → Automation and turn on Local automation.\n",
|
|
377
687
|
);
|
|
378
688
|
return 3;
|
|
379
689
|
}
|
|
@@ -389,7 +699,7 @@ async function main() {
|
|
|
389
699
|
// Build the call envelope. Everything after the command is mapped
|
|
390
700
|
// into the `args` object — the value `--slots='{"a":1}'` becomes
|
|
391
701
|
// `args.slots = {a:1}` thanks to parseScalar.
|
|
392
|
-
const callBody = { command, args:
|
|
702
|
+
const callBody = { command, args: resolved.args };
|
|
393
703
|
const result = await fetchJson(creds.port, "/v1/call", {
|
|
394
704
|
method: "POST",
|
|
395
705
|
headers: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@writepanda/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.183.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",
|