@madewithremy/admin 0.1.40 → 0.1.42

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @madewithremy/admin
2
2
 
3
- Admin SDK + CLI for managing production Remy apps: request logs, analytics, releases, users, secrets, database, files, email (including custom sending/receiving domains), scheduled jobs, QA replays, and more. Almost every operation wraps the same management API the dashboard uses, so anything answerable in the console is answerable here — from the terminal or from code. The exception is `qa-recordings export`, which renders a video on the dev box and therefore only runs inside a sandbox.
3
+ Admin SDK + CLI for managing production Remy apps: request logs, analytics, releases, users, secrets, database, files, email (including custom sending/receiving domains), scheduled jobs, QA replays, and more. Almost every operation wraps the same management API the dashboard uses, so anything answerable in the console is answerable here — from the terminal or from code. The exception is `qa-recordings export`, which the dev box that recorded the replay packages for the platform to render, and which therefore only runs inside a sandbox.
4
4
 
5
5
  One surface, two skins: every CLI command (`remy-admin <group> <sub>`) is also a typed client method (`admin.<group>.<sub>()`). Both are thin layers over the same ops core.
6
6
 
package/dist/index.d.ts CHANGED
@@ -7378,7 +7378,7 @@ declare namespace prerender {
7378
7378
  * roles/http/routes/Apps/develop/recordings.ts
7379
7379
  * customerApps/AppDevBoxes/recordings/sessions.ts
7380
7380
  *
7381
- * Export is the exception — it renders on the dev box (see ../ops/recordings).
7381
+ * Export is the exception — the dev box packages it (see ../ops/qaRecordings).
7382
7382
  */
7383
7383
  /** One document lifetime within a session: a page load and everything after it
7384
7384
  * until the next one. Its lowest-seq chunk carries the rrweb FullSnapshot. */
@@ -7430,13 +7430,13 @@ interface QaRecordingsExportResult {
7430
7430
  * own.
7431
7431
  *
7432
7432
  * `export` is the one operation in this SDK that does NOT go through the
7433
- * management API. Rendering a replay means playing it in a real browser and
7434
- * encoding the frames, and the only machine set up to do that is the dev box
7435
- * the recording was made on: it already has the headless Chrome, the rrweb
7436
- * replayer and ffmpeg. So this talks to the sandbox's local sidecar, and says
7437
- * so plainly when there isn't one. (CLI-only paths are established here —
7433
+ * management API. A replay names the app's own files as the dev box that
7434
+ * recorded it served them, signed in, so that box is what packages it for the
7435
+ * platform to render: it has the proxy those files come from and the browser
7436
+ * session they need. So this talks to the sandbox's local sidecar, and says so
7437
+ * plainly when there isn't one. (CLI-only paths are established here —
7438
7438
  * `methods invoke --stream`, `jewels export --file` — but this is the first
7439
- * that needs local compute rather than local disk.)
7439
+ * that needs the local box rather than local disk.)
7440
7440
  */
7441
7441
 
7442
7442
  /** The app store QA replay chunks are written to (private). */
@@ -7468,13 +7468,19 @@ interface QaRecordingsExportParams {
7468
7468
  access?: 'public' | 'private';
7469
7469
  /** Milliseconds to wait for the render. */
7470
7470
  timeoutMs?: number;
7471
+ /**
7472
+ * How far the render is, as it runs: a line when it moves to a new phase
7473
+ * and every 10% after, e.g. `Rendering the replay… 40%`.
7474
+ */
7475
+ onProgress?: (message: string) => void;
7471
7476
  }
7472
7477
  /**
7473
- * Render a window of a recording session to an mp4 on the dev box.
7478
+ * Render a window of a recording session to an mp4.
7474
7479
  *
7475
- * Blocks for the whole render — the clip plays in real time, then encodes and
7476
- * uploads — and answers with where the video landed. Runs only on a Remy dev
7477
- * box, since that is where the browser and ffmpeg are.
7480
+ * Blocks for the whole render — the box packages the replay, the platform
7481
+ * renders it, which takes minutes for a long clip — and answers with where
7482
+ * the video landed. Runs only on a Remy dev box, since the replay's own files
7483
+ * are served by the box that recorded it.
7478
7484
  *
7479
7485
  * @example
7480
7486
  * const { url } = await admin.qaRecordings.exportVideo({
package/dist/index.js CHANGED
@@ -2727,7 +2727,12 @@ function seqOf(key) {
2727
2727
  );
2728
2728
  return Number.isFinite(n) ? n : 0;
2729
2729
  }
2730
- var DEFAULT_EXPORT_TIMEOUT_MS = 66e4;
2730
+ var DEFAULT_EXPORT_TIMEOUT_MS = 33 * 6e4;
2731
+ var EXPORT_PHASE_LABEL = {
2732
+ loading: "Preparing the replay",
2733
+ rendering: "Rendering the replay",
2734
+ encoding: "Encoding the video"
2735
+ };
2731
2736
  async function exportVideo(_ctx, params) {
2732
2737
  const body = {
2733
2738
  recordingSessionId: params.sessionId,
@@ -2740,7 +2745,12 @@ async function exportVideo(_ctx, params) {
2740
2745
  try {
2741
2746
  res = await fetch(`${SIDECAR_URL}/export-recording`, {
2742
2747
  method: "POST",
2743
- headers: { "Content-Type": "application/json" },
2748
+ // A line per progress event, then the answer (the sidecar's
2749
+ // streamRecordingExport).
2750
+ headers: {
2751
+ "Content-Type": "application/json",
2752
+ Accept: "application/x-ndjson"
2753
+ },
2744
2754
  body: JSON.stringify(body),
2745
2755
  signal: AbortSignal.timeout(
2746
2756
  params.timeoutMs ?? DEFAULT_EXPORT_TIMEOUT_MS
@@ -2751,12 +2761,53 @@ async function exportVideo(_ctx, params) {
2751
2761
  `Could not reach the sandbox at ${SIDECAR_URL} (${err instanceof Error ? err.message : String(err)}). Rendering a replay needs the dev box that recorded it \u2014 this command only works inside a Remy sandbox.`
2752
2762
  );
2753
2763
  }
2754
- const payload = await res.json().catch(() => ({}));
2755
- if (!res.ok) {
2764
+ if (!res.ok || !res.body) {
2765
+ const refused = await res.json().catch(() => ({}));
2756
2766
  throw new Error(
2757
- typeof payload.error === "string" ? payload.error : `The sandbox refused the export (HTTP ${res.status}).`
2767
+ typeof refused.error === "string" ? refused.error : `The sandbox refused the export (HTTP ${res.status}).`
2758
2768
  );
2759
2769
  }
2770
+ let payload = {};
2771
+ let shown = null;
2772
+ const decoder = new TextDecoder();
2773
+ let buffer = "";
2774
+ const read = (text) => {
2775
+ if (!text.trim()) {
2776
+ return;
2777
+ }
2778
+ let value;
2779
+ try {
2780
+ value = JSON.parse(text);
2781
+ } catch {
2782
+ return;
2783
+ }
2784
+ const progress = value.progress;
2785
+ if (!progress) {
2786
+ payload = value;
2787
+ return;
2788
+ }
2789
+ const phase = progress.phase ?? "rendering";
2790
+ const percent = Math.round(progress.percent ?? 0);
2791
+ const step = Math.floor(percent / 10);
2792
+ if (shown?.phase !== phase || step > shown.step) {
2793
+ shown = { phase, step };
2794
+ params.onProgress?.(
2795
+ `${EXPORT_PHASE_LABEL[phase] ?? EXPORT_PHASE_LABEL.rendering}\u2026 ${percent}%`
2796
+ );
2797
+ }
2798
+ };
2799
+ for await (const chunk of res.body) {
2800
+ buffer += decoder.decode(chunk, { stream: true });
2801
+ let end;
2802
+ while ((end = buffer.indexOf("\n")) !== -1) {
2803
+ read(buffer.slice(0, end));
2804
+ buffer = buffer.slice(end + 1);
2805
+ }
2806
+ }
2807
+ read(buffer);
2808
+ if (typeof payload.error === "string") {
2809
+ throw new Error(payload.error);
2810
+ }
2760
2811
  const status2 = payload.export;
2761
2812
  if (!status2) {
2762
2813
  throw new Error(