@madewithremy/admin 0.1.25 → 0.1.27
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 +2 -1
- package/dist/index.d.ts +137 -10
- package/dist/index.js +108 -0
- package/dist/index.js.map +1 -1
- package/dist/prod.js +252 -42
- package/package.json +7 -6
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, and more.
|
|
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.
|
|
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
|
|
|
@@ -28,6 +28,7 @@ Configuration is read from the environment and the workspace — there are no co
|
|
|
28
28
|
| `API_BASE_URL` | API origin (default `https://api.mindstudio.ai`) |
|
|
29
29
|
| `WORKSPACE_DIR` | Workspace root (default `/home/vercel-sandbox/workspace`) |
|
|
30
30
|
| `${WORKSPACE_DIR}/mindstudio.json` | `appId` — which app the CLI manages |
|
|
31
|
+
| `REMY_SIDECAR_URL` | Sandbox sidecar for `qa-recordings export` (default `http://127.0.0.1:4388`) |
|
|
31
32
|
|
|
32
33
|
## Import layer
|
|
33
34
|
|
package/dist/index.d.ts
CHANGED
|
@@ -635,16 +635,17 @@ interface EventsTailFrame {
|
|
|
635
635
|
|
|
636
636
|
interface EventsScopeParams {
|
|
637
637
|
/**
|
|
638
|
-
* Environment scope to inspect: `live` (default),
|
|
639
|
-
* or `dev:<sessionId>` — publishes and subscriptions
|
|
640
|
-
* so a
|
|
638
|
+
* Environment scope to inspect: `live` (the default for reads),
|
|
639
|
+
* `preview:<releaseId>`, or `dev:<sessionId>` — publishes and subscriptions
|
|
640
|
+
* never cross scopes, so a dev session's events are visible only under its
|
|
641
|
+
* `dev:` scope. A publish without one goes to the caller's own dev session.
|
|
641
642
|
*/
|
|
642
643
|
scope?: string;
|
|
643
644
|
}
|
|
644
645
|
interface EventsPublishParams extends EventsScopeParams {
|
|
645
646
|
/** Exact channel name(s) — no wildcards anywhere in app events. */
|
|
646
647
|
channels: string | string[];
|
|
647
|
-
/** The payload subscribers receive (≤
|
|
648
|
+
/** The payload subscribers receive (≤256k serialized characters). */
|
|
648
649
|
data: unknown;
|
|
649
650
|
}
|
|
650
651
|
/**
|
|
@@ -665,10 +666,13 @@ declare function channelsList(ctx: AdminContext, params?: EventsScopeParams): Pr
|
|
|
665
666
|
* same validation, same delivery, same metering. Lets a frontend subscriber
|
|
666
667
|
* be verified before the backend trigger exists.
|
|
667
668
|
*
|
|
668
|
-
* `
|
|
669
|
-
* `
|
|
669
|
+
* Without `scope` it goes to the caller's own dev session, the preview they
|
|
670
|
+
* are building in; `scope: 'live'` reaches real users. The result's `scope`
|
|
671
|
+
* says where it went. `delivered` counts live subscriber connections per
|
|
672
|
+
* published channel; `0` is normal (nobody listening), never an error.
|
|
670
673
|
*
|
|
671
|
-
* @throws AdminApiError `invalid_channels` / `payload_too_large` (400)
|
|
674
|
+
* @throws AdminApiError `invalid_channels` / `payload_too_large` (400), and
|
|
675
|
+
* `no_dev_session` (400) with no `scope` when the caller has no dev session.
|
|
672
676
|
* @example
|
|
673
677
|
* const { delivered } = await admin.events.publish({
|
|
674
678
|
* channels: 'jobs:usr_123',
|
|
@@ -4660,7 +4664,7 @@ interface FilesLsParams {
|
|
|
4660
4664
|
* prefix: 'images/',
|
|
4661
4665
|
* });
|
|
4662
4666
|
*/
|
|
4663
|
-
declare function ls(ctx: AdminContext, params: FilesLsParams): Promise<FilesLsResult>;
|
|
4667
|
+
declare function ls$1(ctx: AdminContext, params: FilesLsParams): Promise<FilesLsResult>;
|
|
4664
4668
|
/**
|
|
4665
4669
|
* Store-level summary: every store with object counts, total bytes, and
|
|
4666
4670
|
* AV/PII scan status histograms.
|
|
@@ -4684,14 +4688,13 @@ type files_FileRef = FileRef;
|
|
|
4684
4688
|
type files_FilesLsParams = FilesLsParams;
|
|
4685
4689
|
type files_FilesPutParams = FilesPutParams;
|
|
4686
4690
|
declare const files_fetchBytes: typeof fetchBytes;
|
|
4687
|
-
declare const files_ls: typeof ls;
|
|
4688
4691
|
declare const files_put: typeof put;
|
|
4689
4692
|
declare const files_remove: typeof remove;
|
|
4690
4693
|
declare const files_sign: typeof sign;
|
|
4691
4694
|
declare const files_stat: typeof stat;
|
|
4692
4695
|
declare const files_summary: typeof summary;
|
|
4693
4696
|
declare namespace files {
|
|
4694
|
-
export { type files_FileAccess as FileAccess, type files_FileRef as FileRef, type files_FilesLsParams as FilesLsParams, type files_FilesPutParams as FilesPutParams, files_fetchBytes as fetchBytes,
|
|
4697
|
+
export { type files_FileAccess as FileAccess, type files_FileRef as FileRef, type files_FilesLsParams as FilesLsParams, type files_FilesPutParams as FilesPutParams, files_fetchBytes as fetchBytes, ls$1 as ls, files_put as put, files_remove as remove, files_sign as sign, files_stat as stat, files_summary as summary };
|
|
4695
4698
|
}
|
|
4696
4699
|
|
|
4697
4700
|
/**
|
|
@@ -6399,6 +6402,129 @@ declare namespace prerender {
|
|
|
6399
6402
|
export { type prerender_PrerenderInvalidateParams as PrerenderInvalidateParams, type prerender_PrerenderPagesParams as PrerenderPagesParams, type prerender_PrerenderViewParams as PrerenderViewParams, prerender_invalidate as invalidate, prerender_pages as pages, prerender_view as view };
|
|
6400
6403
|
}
|
|
6401
6404
|
|
|
6405
|
+
/**
|
|
6406
|
+
* Response types for browser-QA replay recordings.
|
|
6407
|
+
*
|
|
6408
|
+
* Chunks are ordinary private files in the app's `qa-recordings` store, keyed
|
|
6409
|
+
* `{sessionId}/{runId}/{seq}.json`, so `ls` here is a grouped view over
|
|
6410
|
+
* `files.ls` rather than its own endpoint. Transcribed from youai-api:
|
|
6411
|
+
* src/http/routes/V2Apps/develop/recordings.ts
|
|
6412
|
+
* src/common/Recordings/sessions.ts
|
|
6413
|
+
*
|
|
6414
|
+
* Export is the exception — it renders on the dev box (see ../ops/recordings).
|
|
6415
|
+
*/
|
|
6416
|
+
/** One document lifetime within a session: a page load and everything after it
|
|
6417
|
+
* until the next one. Its lowest-seq chunk carries the rrweb FullSnapshot. */
|
|
6418
|
+
interface QaRecordingRun {
|
|
6419
|
+
runId: string;
|
|
6420
|
+
chunks: number;
|
|
6421
|
+
bytes: number;
|
|
6422
|
+
/** Store-relative keys, in seq order. */
|
|
6423
|
+
keys: string[];
|
|
6424
|
+
}
|
|
6425
|
+
/** One recording session — a single tunnel process's continuous recording. */
|
|
6426
|
+
interface QaRecordingSession {
|
|
6427
|
+
sessionId: string;
|
|
6428
|
+
chunks: number;
|
|
6429
|
+
bytes: number;
|
|
6430
|
+
/** ISO-8601, from the chunk objects' last-modified times. */
|
|
6431
|
+
startedAt?: string;
|
|
6432
|
+
endedAt?: string;
|
|
6433
|
+
runs: QaRecordingRun[];
|
|
6434
|
+
}
|
|
6435
|
+
interface QaRecordingsLsResult {
|
|
6436
|
+
store: string;
|
|
6437
|
+
sessions: QaRecordingSession[];
|
|
6438
|
+
/** Present when the listing was truncated — pass it back as `cursor`. */
|
|
6439
|
+
cursor?: string;
|
|
6440
|
+
}
|
|
6441
|
+
interface QaRecordingsExportResult {
|
|
6442
|
+
jobId: string;
|
|
6443
|
+
status: 'completed' | 'failed' | 'cancelled';
|
|
6444
|
+
/** Public target only. A private one gives `store`/`key`; sign it with
|
|
6445
|
+
* `files sign` when you want a link. */
|
|
6446
|
+
url?: string;
|
|
6447
|
+
store?: string;
|
|
6448
|
+
key?: string;
|
|
6449
|
+
width?: number;
|
|
6450
|
+
height?: number;
|
|
6451
|
+
/** Length of the rendered clip, dead air already collapsed. */
|
|
6452
|
+
durationMs?: number;
|
|
6453
|
+
error?: string;
|
|
6454
|
+
errorCode?: string;
|
|
6455
|
+
}
|
|
6456
|
+
|
|
6457
|
+
/**
|
|
6458
|
+
* Browser-QA replay recordings: list what the QA agent recorded, and render a
|
|
6459
|
+
* replay to an mp4.
|
|
6460
|
+
*
|
|
6461
|
+
* `ls` is a grouped view over `files.ls` — a replay chunk is an ordinary
|
|
6462
|
+
* private file in the app's `qa-recordings` store and needs no endpoint of its
|
|
6463
|
+
* own.
|
|
6464
|
+
*
|
|
6465
|
+
* `export` is the one operation in this SDK that does NOT go through the
|
|
6466
|
+
* management API. Rendering a replay means playing it in a real browser and
|
|
6467
|
+
* encoding the frames, and the only machine set up to do that is the dev box
|
|
6468
|
+
* the recording was made on: it already has the headless Chrome, the rrweb
|
|
6469
|
+
* replayer and ffmpeg. So this talks to the sandbox's local sidecar, and says
|
|
6470
|
+
* so plainly when there isn't one. (CLI-only paths are established here —
|
|
6471
|
+
* `methods invoke --stream`, `jewels export --file` — but this is the first
|
|
6472
|
+
* that needs local compute rather than local disk.)
|
|
6473
|
+
*/
|
|
6474
|
+
|
|
6475
|
+
/** The app store QA replay chunks are written to (private). */
|
|
6476
|
+
declare const RECORDING_STORE = "qa-recordings";
|
|
6477
|
+
interface QaRecordingsLsParams {
|
|
6478
|
+
/** Only this session. Omitted → every session in the store. */
|
|
6479
|
+
sessionId?: string;
|
|
6480
|
+
cursor?: string;
|
|
6481
|
+
/** Chunk objects to scan, not sessions returned. */
|
|
6482
|
+
limit?: number;
|
|
6483
|
+
}
|
|
6484
|
+
/**
|
|
6485
|
+
* Recording sessions in this app, grouped from the chunk keys.
|
|
6486
|
+
*
|
|
6487
|
+
* @example
|
|
6488
|
+
* const { sessions } = await admin.qaRecordings.ls({});
|
|
6489
|
+
* // → [{ sessionId, chunks, bytes, runs: [{ runId, keys }] }]
|
|
6490
|
+
*/
|
|
6491
|
+
declare function ls(ctx: AdminContext, params?: QaRecordingsLsParams): Promise<QaRecordingsLsResult>;
|
|
6492
|
+
interface QaRecordingsExportParams {
|
|
6493
|
+
/** The recording session to render (32 hex characters). */
|
|
6494
|
+
sessionId: string;
|
|
6495
|
+
/** Window of the session, as epoch milliseconds. Both required: a replay is
|
|
6496
|
+
* a slice of a continuous recording, not a file. */
|
|
6497
|
+
from: number;
|
|
6498
|
+
to: number;
|
|
6499
|
+
/** Where the mp4 is written. Defaults to `assets`, public. */
|
|
6500
|
+
store?: string;
|
|
6501
|
+
access?: 'public' | 'private';
|
|
6502
|
+
/** Milliseconds to wait for the render. */
|
|
6503
|
+
timeoutMs?: number;
|
|
6504
|
+
}
|
|
6505
|
+
/**
|
|
6506
|
+
* Render a window of a recording session to an mp4 on the dev box.
|
|
6507
|
+
*
|
|
6508
|
+
* Blocks for the whole render — the clip plays in real time, then encodes and
|
|
6509
|
+
* uploads — and answers with where the video landed. Runs only on a Remy dev
|
|
6510
|
+
* box, since that is where the browser and ffmpeg are.
|
|
6511
|
+
*
|
|
6512
|
+
* @example
|
|
6513
|
+
* const { url } = await admin.qaRecordings.exportVideo({
|
|
6514
|
+
* sessionId, from: 1758000000000, to: 1758000042000,
|
|
6515
|
+
* });
|
|
6516
|
+
*/
|
|
6517
|
+
declare function exportVideo(_ctx: AdminContext, params: QaRecordingsExportParams): Promise<QaRecordingsExportResult>;
|
|
6518
|
+
|
|
6519
|
+
type qaRecordings_QaRecordingsExportParams = QaRecordingsExportParams;
|
|
6520
|
+
type qaRecordings_QaRecordingsLsParams = QaRecordingsLsParams;
|
|
6521
|
+
declare const qaRecordings_RECORDING_STORE: typeof RECORDING_STORE;
|
|
6522
|
+
declare const qaRecordings_exportVideo: typeof exportVideo;
|
|
6523
|
+
declare const qaRecordings_ls: typeof ls;
|
|
6524
|
+
declare namespace qaRecordings {
|
|
6525
|
+
export { type qaRecordings_QaRecordingsExportParams as QaRecordingsExportParams, type qaRecordings_QaRecordingsLsParams as QaRecordingsLsParams, qaRecordings_RECORDING_STORE as RECORDING_STORE, qaRecordings_exportVideo as exportVideo, qaRecordings_ls as ls };
|
|
6526
|
+
}
|
|
6527
|
+
|
|
6402
6528
|
/**
|
|
6403
6529
|
* Release operations: list, get, by-commit, dashboard live-release, and the
|
|
6404
6530
|
* composite waitForCommit op that owns the two poll loops.
|
|
@@ -7590,6 +7716,7 @@ declare function buildClient(ctx: AdminContext): {
|
|
|
7590
7716
|
jewels: BoundOps<typeof jewels>;
|
|
7591
7717
|
methods: BoundOps<typeof methods>;
|
|
7592
7718
|
prerender: BoundOps<typeof prerender>;
|
|
7719
|
+
qaRecordings: BoundOps<typeof qaRecordings>;
|
|
7593
7720
|
releases: BoundOps<typeof releases>;
|
|
7594
7721
|
requests: BoundOps<typeof requests>;
|
|
7595
7722
|
secrets: BoundOps<typeof secrets>;
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import path from "path";
|
|
|
10
10
|
|
|
11
11
|
// src/config.ts
|
|
12
12
|
var WORKSPACE_DIR = process.env["WORKSPACE_DIR"] || "/home/remy/workspace";
|
|
13
|
+
var SIDECAR_URL = process.env["REMY_SIDECAR_URL"] || "http://127.0.0.1:4388";
|
|
13
14
|
|
|
14
15
|
// src/parseJsonConfig.ts
|
|
15
16
|
import JSON5 from "json5";
|
|
@@ -2415,6 +2416,112 @@ function invalidate(ctx, params = {}) {
|
|
|
2415
2416
|
);
|
|
2416
2417
|
}
|
|
2417
2418
|
|
|
2419
|
+
// src/ops/qaRecordings.ts
|
|
2420
|
+
var qaRecordings_exports = {};
|
|
2421
|
+
__export(qaRecordings_exports, {
|
|
2422
|
+
RECORDING_STORE: () => RECORDING_STORE,
|
|
2423
|
+
exportVideo: () => exportVideo,
|
|
2424
|
+
ls: () => ls2
|
|
2425
|
+
});
|
|
2426
|
+
var RECORDING_STORE = "qa-recordings";
|
|
2427
|
+
async function ls2(ctx, params = {}) {
|
|
2428
|
+
const page = await ls(ctx, {
|
|
2429
|
+
store: RECORDING_STORE,
|
|
2430
|
+
access: "private",
|
|
2431
|
+
...params.sessionId ? { prefix: `${params.sessionId}/` } : {},
|
|
2432
|
+
...params.cursor ? { cursor: params.cursor } : {},
|
|
2433
|
+
...params.limit ? { limit: params.limit } : {}
|
|
2434
|
+
});
|
|
2435
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
2436
|
+
for (const entry of page.files) {
|
|
2437
|
+
const parts = entry.key.split("/");
|
|
2438
|
+
if (parts.length !== 3) {
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
const [sessionId, runId] = parts;
|
|
2442
|
+
let session = sessions.get(sessionId);
|
|
2443
|
+
if (!session) {
|
|
2444
|
+
session = { sessionId, chunks: 0, bytes: 0, runs: [] };
|
|
2445
|
+
sessions.set(sessionId, session);
|
|
2446
|
+
}
|
|
2447
|
+
let run3 = session.runs.find((r) => r.runId === runId);
|
|
2448
|
+
if (!run3) {
|
|
2449
|
+
run3 = { runId, chunks: 0, bytes: 0, keys: [] };
|
|
2450
|
+
session.runs.push(run3);
|
|
2451
|
+
}
|
|
2452
|
+
const size = entry.size ?? 0;
|
|
2453
|
+
session.chunks += 1;
|
|
2454
|
+
session.bytes += size;
|
|
2455
|
+
run3.chunks += 1;
|
|
2456
|
+
run3.bytes += size;
|
|
2457
|
+
run3.keys.push(entry.key);
|
|
2458
|
+
if (entry.updatedAt) {
|
|
2459
|
+
if (!session.startedAt || entry.updatedAt < session.startedAt) {
|
|
2460
|
+
session.startedAt = entry.updatedAt;
|
|
2461
|
+
}
|
|
2462
|
+
if (!session.endedAt || entry.updatedAt > session.endedAt) {
|
|
2463
|
+
session.endedAt = entry.updatedAt;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
for (const session of sessions.values()) {
|
|
2468
|
+
for (const run3 of session.runs) {
|
|
2469
|
+
run3.keys.sort((a, b) => seqOf(a) - seqOf(b));
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
return {
|
|
2473
|
+
store: RECORDING_STORE,
|
|
2474
|
+
sessions: [...sessions.values()].sort(
|
|
2475
|
+
(a, b) => (b.endedAt ?? "").localeCompare(a.endedAt ?? "")
|
|
2476
|
+
),
|
|
2477
|
+
...page.cursor ? { cursor: page.cursor } : {}
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
function seqOf(key) {
|
|
2481
|
+
const n = Number(
|
|
2482
|
+
key.split("/").pop()?.replace(/\.json$/, "")
|
|
2483
|
+
);
|
|
2484
|
+
return Number.isFinite(n) ? n : 0;
|
|
2485
|
+
}
|
|
2486
|
+
var DEFAULT_EXPORT_TIMEOUT_MS = 66e4;
|
|
2487
|
+
async function exportVideo(_ctx, params) {
|
|
2488
|
+
const body = {
|
|
2489
|
+
recordingSessionId: params.sessionId,
|
|
2490
|
+
startTs: params.from,
|
|
2491
|
+
endTs: params.to,
|
|
2492
|
+
...params.store ? { store: params.store } : {},
|
|
2493
|
+
...params.access ? { access: params.access } : {}
|
|
2494
|
+
};
|
|
2495
|
+
let res;
|
|
2496
|
+
try {
|
|
2497
|
+
res = await fetch(`${SIDECAR_URL}/export-recording`, {
|
|
2498
|
+
method: "POST",
|
|
2499
|
+
headers: { "Content-Type": "application/json" },
|
|
2500
|
+
body: JSON.stringify(body),
|
|
2501
|
+
signal: AbortSignal.timeout(
|
|
2502
|
+
params.timeoutMs ?? DEFAULT_EXPORT_TIMEOUT_MS
|
|
2503
|
+
)
|
|
2504
|
+
});
|
|
2505
|
+
} catch (err) {
|
|
2506
|
+
throw new Error(
|
|
2507
|
+
`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.`
|
|
2508
|
+
);
|
|
2509
|
+
}
|
|
2510
|
+
const payload = await res.json().catch(() => ({}));
|
|
2511
|
+
if (!res.ok) {
|
|
2512
|
+
throw new Error(
|
|
2513
|
+
typeof payload.error === "string" ? payload.error : `The sandbox refused the export (HTTP ${res.status}).`
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
const status2 = payload.export;
|
|
2517
|
+
if (!status2) {
|
|
2518
|
+
throw new Error(
|
|
2519
|
+
"The sandbox accepted the export but reported no result. Check the box's tunnel logs."
|
|
2520
|
+
);
|
|
2521
|
+
}
|
|
2522
|
+
return status2;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2418
2525
|
// src/ops/requests.ts
|
|
2419
2526
|
var requests_exports = {};
|
|
2420
2527
|
__export(requests_exports, {
|
|
@@ -2748,6 +2855,7 @@ function buildClient(ctx) {
|
|
|
2748
2855
|
jewels: bindOps(jewels_exports, ctx),
|
|
2749
2856
|
methods: bindOps(methods_exports, ctx),
|
|
2750
2857
|
prerender: bindOps(prerender_exports, ctx),
|
|
2858
|
+
qaRecordings: bindOps(qaRecordings_exports, ctx),
|
|
2751
2859
|
releases: bindOps(releases_exports, ctx),
|
|
2752
2860
|
requests: bindOps(requests_exports, ctx),
|
|
2753
2861
|
secrets: bindOps(secrets_exports, ctx),
|