@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/dist/prod.js CHANGED
@@ -987,6 +987,7 @@ import { execFileSync } from "child_process";
987
987
 
988
988
  // src/config.ts
989
989
  var WORKSPACE_DIR = process.env["WORKSPACE_DIR"] || "/home/remy/workspace";
990
+ var SIDECAR_URL = process.env["REMY_SIDECAR_URL"] || "http://127.0.0.1:4388";
990
991
 
991
992
  // src/ops/releases.ts
992
993
  function list3(ctx, params = {}) {
@@ -2235,7 +2236,7 @@ function liftFromLive(ctx, params = {}) {
2235
2236
  // src/commands/data.ts
2236
2237
  var dataSpecs = {
2237
2238
  "data lift-from-dev": {
2238
- usage: "Usage: remy-admin data lift-from-dev <appId> --confirm [--release-id <id>]\nRefusing to run without the exact appId and --confirm: this destructively replaces the live release's databases with a snapshot of dev. Wipes any rows live had \u2014 including signed-up users. Intended for first-publish / pre-launch data sync only.\n--release-id names which of your dev workspaces to lift from, and is required only when you have more than one open (a box and a laptop CLI). The ambiguity error lists the ids.",
2239
+ usage: "Usage: remy-admin data lift-from-dev <appId> --confirm [--release-id <id>]\nRefusing to run without the exact appId and --confirm: this destructively replaces the live release's databases with a snapshot of dev. Wipes any rows live had \u2014 including signed-up users. Intended for first-publish / pre-launch data sync only.\n--release-id names the dev release to lift from; without it, your dev workspace in this app.",
2239
2240
  positionals: [{ name: "appId", required: true }],
2240
2241
  flags: { confirm: { type: "boolean" }, "release-id": { type: "string" } },
2241
2242
  requireAnyOf: {
@@ -2244,7 +2245,7 @@ var dataSpecs = {
2244
2245
  }
2245
2246
  },
2246
2247
  "data lift-from-live": {
2247
- usage: "Usage: remy-admin data lift-from-live [--truncate] --confirm [--release-id <id>]\nRefusing to run without --confirm: this destructively replaces the dev release's databases with a snapshot of live (wiping local dev data). Live/prod data is never touched. With --truncate it instead empties the dev databases (keeps schema, no data pulled from live).\n--release-id names which of your dev workspaces to overwrite, and is required only when you have more than one open.",
2248
+ usage: "Usage: remy-admin data lift-from-live [--truncate] --confirm [--release-id <id>]\nRefusing to run without --confirm: this destructively replaces the dev release's databases with a snapshot of live (wiping local dev data). Live/prod data is never touched. With --truncate it instead empties the dev databases (keeps schema, no data pulled from live).\n--release-id names the dev release to overwrite; without it, your dev workspace in this app.",
2248
2249
  flags: {
2249
2250
  truncate: { type: "boolean" },
2250
2251
  confirm: { type: "boolean" },
@@ -6480,7 +6481,7 @@ var eventsSpecs = {
6480
6481
  }
6481
6482
  },
6482
6483
  "events publish": {
6483
- usage: "Usage: remy-admin events publish <channel> <json> [--scope live]\nPublishes exactly as the app's own events.publish would \u2014 same validation, delivery, and metering.",
6484
+ usage: "Usage: remy-admin events publish <channel> <json> [--scope live]\nPublishes exactly as the app's own events.publish would \u2014 same validation, delivery, and metering. Goes to your own dev session unless --scope says otherwise; --scope live reaches real users.",
6484
6485
  positionals: [
6485
6486
  { name: "channel", required: true },
6486
6487
  { name: "json", required: true }
@@ -6567,18 +6568,21 @@ Notes:
6567
6568
  run from a script.
6568
6569
 
6569
6570
  publish lets a frontend subscriber be verified before the backend trigger
6570
- exists. The response's 'delivered' counts live subscriber connections per
6571
- published channel \u2014 0 means nobody is listening right now, which is normal,
6572
- not an error.
6571
+ exists. It goes to YOUR dev session by default (the preview you're building
6572
+ in); pass --scope live to reach real users' browsers. The response's
6573
+ 'scope' says where it went, and 'delivered' counts live subscriber
6574
+ connections per published channel \u2014 0 means nobody is listening right now,
6575
+ which is normal, not an error.
6573
6576
 
6574
6577
  channels list is the mismatch-finder: a channel with subscribers but no
6575
6578
  recent publish (or the reverse) usually means the frontend and backend spell
6576
6579
  the channel differently. Window is 24h; channels are exact strings \u2014 there
6577
6580
  are no wildcards anywhere in app events.
6578
6581
 
6579
- Scopes: events never cross execution environments. --scope defaults to
6580
- 'live'; use 'dev:<devSessionId>' to inspect a tunnel session's world or
6581
- 'preview:<releaseId>' for a branch preview.`;
6582
+ Scopes: events never cross execution environments. tail and channels list
6583
+ default to 'live', publish to your own dev session; use
6584
+ 'dev:<devSessionId>' for a dev session's world (a publish prints its own)
6585
+ or 'preview:<releaseId>' for a branch preview.`;
6582
6586
 
6583
6587
  // src/ops/jewels.ts
6584
6588
  var JEWEL_RUN_TIMEOUT_MS = 6e5;
@@ -8180,11 +8184,13 @@ Today the one kind is dedicated retrieval: the app's own vector-store capacity
8180
8184
  for data sources, isolated from the shared pool. Sizes and prices come from the
8181
8185
  platform catalog (\`infra list\` prints them); nothing is priced client-side.
8182
8186
 
8183
- A resource bills by the hour from activation, at the retained-storage rate while
8184
- hibernated, and not at all once destroyed. Provisioning (and growing) needs a
8185
- month's worth of credits available in the workspace, checked before the request
8186
- is accepted; nothing is charged until activation. The provision output prints
8187
- the price it committed to. Every action below is audited.
8187
+ A resource bills by the clock hour at the active rate for every hour it is
8188
+ active in, including the hour a resume lands in, and costs nothing while
8189
+ hibernated or once destroyed. Provisioning, growing and resuming need a month's
8190
+ worth of credits available in the workspace, checked before the request is
8191
+ accepted (insufficient_credits); nothing is charged until activation. The
8192
+ provision output prints the price it committed to. Every action below is
8193
+ audited.
8188
8194
 
8189
8195
  Subcommands:
8190
8196
  list Resources on this app, plus the offerings you can provision
@@ -8192,7 +8198,7 @@ Subcommands:
8192
8198
  logs The platform's log for a resource (--follow to keep reading)
8193
8199
  provision Lease a new resource (needs a month's credits; prints the price)
8194
8200
  hibernate Park it: data kept, searches paused, compute charge stops
8195
- resume Bring a hibernated resource back
8201
+ resume Bring a hibernated resource back (needs a month's credits)
8196
8202
  resize Change its size, data intact (passes through hibernated)
8197
8203
  destroy Delete it (refused while data sources are placed on it)
8198
8204
  rename Change the display name
@@ -8263,6 +8269,205 @@ Billing and the log:
8263
8269
  --wait exit codes: 0 reached the target phase \xB7 1 the platform reported failure \xB7
8264
8270
  2 still transitioning when --timeout elapsed (default 600s).`;
8265
8271
 
8272
+ // src/ops/qaRecordings.ts
8273
+ var RECORDING_STORE = "qa-recordings";
8274
+ async function ls2(ctx, params = {}) {
8275
+ const page = await ls(ctx, {
8276
+ store: RECORDING_STORE,
8277
+ access: "private",
8278
+ ...params.sessionId ? { prefix: `${params.sessionId}/` } : {},
8279
+ ...params.cursor ? { cursor: params.cursor } : {},
8280
+ ...params.limit ? { limit: params.limit } : {}
8281
+ });
8282
+ const sessions = /* @__PURE__ */ new Map();
8283
+ for (const entry of page.files) {
8284
+ const parts = entry.key.split("/");
8285
+ if (parts.length !== 3) {
8286
+ continue;
8287
+ }
8288
+ const [sessionId, runId] = parts;
8289
+ let session = sessions.get(sessionId);
8290
+ if (!session) {
8291
+ session = { sessionId, chunks: 0, bytes: 0, runs: [] };
8292
+ sessions.set(sessionId, session);
8293
+ }
8294
+ let run3 = session.runs.find((r) => r.runId === runId);
8295
+ if (!run3) {
8296
+ run3 = { runId, chunks: 0, bytes: 0, keys: [] };
8297
+ session.runs.push(run3);
8298
+ }
8299
+ const size = entry.size ?? 0;
8300
+ session.chunks += 1;
8301
+ session.bytes += size;
8302
+ run3.chunks += 1;
8303
+ run3.bytes += size;
8304
+ run3.keys.push(entry.key);
8305
+ if (entry.updatedAt) {
8306
+ if (!session.startedAt || entry.updatedAt < session.startedAt) {
8307
+ session.startedAt = entry.updatedAt;
8308
+ }
8309
+ if (!session.endedAt || entry.updatedAt > session.endedAt) {
8310
+ session.endedAt = entry.updatedAt;
8311
+ }
8312
+ }
8313
+ }
8314
+ for (const session of sessions.values()) {
8315
+ for (const run3 of session.runs) {
8316
+ run3.keys.sort((a, b) => seqOf(a) - seqOf(b));
8317
+ }
8318
+ }
8319
+ return {
8320
+ store: RECORDING_STORE,
8321
+ sessions: [...sessions.values()].sort(
8322
+ (a, b) => (b.endedAt ?? "").localeCompare(a.endedAt ?? "")
8323
+ ),
8324
+ ...page.cursor ? { cursor: page.cursor } : {}
8325
+ };
8326
+ }
8327
+ function seqOf(key) {
8328
+ const n = Number(
8329
+ key.split("/").pop()?.replace(/\.json$/, "")
8330
+ );
8331
+ return Number.isFinite(n) ? n : 0;
8332
+ }
8333
+ var DEFAULT_EXPORT_TIMEOUT_MS = 66e4;
8334
+ async function exportVideo(_ctx, params) {
8335
+ const body = {
8336
+ recordingSessionId: params.sessionId,
8337
+ startTs: params.from,
8338
+ endTs: params.to,
8339
+ ...params.store ? { store: params.store } : {},
8340
+ ...params.access ? { access: params.access } : {}
8341
+ };
8342
+ let res;
8343
+ try {
8344
+ res = await fetch(`${SIDECAR_URL}/export-recording`, {
8345
+ method: "POST",
8346
+ headers: { "Content-Type": "application/json" },
8347
+ body: JSON.stringify(body),
8348
+ signal: AbortSignal.timeout(
8349
+ params.timeoutMs ?? DEFAULT_EXPORT_TIMEOUT_MS
8350
+ )
8351
+ });
8352
+ } catch (err) {
8353
+ throw new Error(
8354
+ `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.`
8355
+ );
8356
+ }
8357
+ const payload = await res.json().catch(() => ({}));
8358
+ if (!res.ok) {
8359
+ throw new Error(
8360
+ typeof payload.error === "string" ? payload.error : `The sandbox refused the export (HTTP ${res.status}).`
8361
+ );
8362
+ }
8363
+ const status2 = payload.export;
8364
+ if (!status2) {
8365
+ throw new Error(
8366
+ "The sandbox accepted the export but reported no result. Check the box's tunnel logs."
8367
+ );
8368
+ }
8369
+ return status2;
8370
+ }
8371
+
8372
+ // src/commands/qaRecordings.ts
8373
+ var qaRecordingsSpecs = {
8374
+ "qa-recordings ls": {
8375
+ usage: "Usage: remy-admin qa-recordings ls [--session <id>] [--cursor <cursor>] [--limit <n>]",
8376
+ flags: {
8377
+ session: { type: "string" },
8378
+ cursor: { type: "string" },
8379
+ limit: { type: "number", min: 1 }
8380
+ }
8381
+ },
8382
+ "qa-recordings export": {
8383
+ usage: "Usage: remy-admin qa-recordings export --session <id> --from <epochMs> --to <epochMs> [--store <name>] [--private]",
8384
+ flags: {
8385
+ session: { type: "string" },
8386
+ from: { type: "number" },
8387
+ to: { type: "number" },
8388
+ store: { type: "string" },
8389
+ private: { type: "boolean" }
8390
+ }
8391
+ }
8392
+ };
8393
+ async function qaRecordingsLs(ctx, a) {
8394
+ out(
8395
+ await ls2(ctx, {
8396
+ sessionId: a.str("session"),
8397
+ cursor: a.str("cursor"),
8398
+ limit: a.num("limit")
8399
+ })
8400
+ );
8401
+ }
8402
+ async function qaRecordingsExport(ctx, a) {
8403
+ const sessionId = a.str("session");
8404
+ const from = a.num("from");
8405
+ const to = a.num("to");
8406
+ if (!sessionId) {
8407
+ fatal("--session is required (see `remy-admin qa-recordings ls`).");
8408
+ }
8409
+ if (from === void 0 || to === void 0) {
8410
+ fatal(
8411
+ "--from and --to are required (epoch milliseconds). A QA run reports both on its result."
8412
+ );
8413
+ }
8414
+ const result = await exportVideo(ctx, {
8415
+ sessionId,
8416
+ from,
8417
+ to,
8418
+ store: a.str("store"),
8419
+ access: a.bool("private") ? "private" : void 0
8420
+ });
8421
+ if (result.status !== "completed") {
8422
+ fatal(
8423
+ result.errorCode === "FFMPEG_UNAVAILABLE" ? "This sandbox has no ffmpeg \u2014 video export needs a box on the current devbox image." : result.error ?? `Export ${result.status}.`
8424
+ );
8425
+ }
8426
+ out(result);
8427
+ }
8428
+ var qaRecordingsHandlers = {
8429
+ "qa-recordings ls": qaRecordingsLs,
8430
+ "qa-recordings export": qaRecordingsExport
8431
+ };
8432
+ var qaRecordingsHelp = `remy-admin qa-recordings \u2014 Browser-QA session replays.
8433
+
8434
+ Every automated browser test records what the page did, as rrweb (a DOM
8435
+ recording, not video). The chunks are ordinary private files in the
8436
+ 'qa-recordings' store, so 'remy-admin files' works on them directly: sign one
8437
+ for a link, download one, or copy one into a public store to embed it.
8438
+
8439
+ 'export' turns a replay into an mp4 \u2014 the shareable artifact, for a changelog
8440
+ entry, a release note or a bug report. It renders on the dev box (real browser,
8441
+ real ffmpeg, no AI-generated frames), so it only works inside a sandbox, it
8442
+ blocks for the length of the clip, and it won't start while a video export is
8443
+ already running.
8444
+
8445
+ Subcommands:
8446
+ ls Recording sessions in this app, grouped into runs
8447
+ export Render a window of a session to an mp4 and print where it landed
8448
+
8449
+ Usage:
8450
+ remy-admin qa-recordings ls [--session <id>] [--limit <n>]
8451
+ remy-admin qa-recordings export --session <id> --from <epochMs> --to <epochMs> [--store <name>] [--private]
8452
+
8453
+ Options:
8454
+ --session Recording session id (32 hex characters)
8455
+ --from/--to The window to render, as epoch milliseconds. A QA run prints the
8456
+ session and both bounds on its result.
8457
+ --store Destination store for the mp4 (default: assets)
8458
+ --private Write the mp4 privately instead of publicly. No URL is returned;
8459
+ use 'files sign' to make one.
8460
+
8461
+ Examples:
8462
+ # What was recorded recently
8463
+ remy-admin qa-recordings ls --limit 200
8464
+
8465
+ # Render a run and get a public URL to embed
8466
+ remy-admin qa-recordings export --session 9f2c\u2026 --from 1758000000000 --to 1758000042000
8467
+
8468
+ # The raw rrweb chunks for a session (download, inspect, or rehost)
8469
+ remy-admin files ls --private --store qa-recordings --prefix 9f2c\u2026/`;
8470
+
8266
8471
  // src/commands/index.ts
8267
8472
  var SPECS = {
8268
8473
  ...requestsSpecs,
@@ -8291,7 +8496,8 @@ var SPECS = {
8291
8496
  ...settingsSpecs,
8292
8497
  ...emailSpecs,
8293
8498
  ...cronSpecs,
8294
- ...infraSpecs
8499
+ ...infraSpecs,
8500
+ ...qaRecordingsSpecs
8295
8501
  };
8296
8502
  var HANDLERS = {
8297
8503
  ...requestsHandlers,
@@ -8320,7 +8526,8 @@ var HANDLERS = {
8320
8526
  ...settingsHandlers,
8321
8527
  ...emailHandlers,
8322
8528
  ...cronHandlers,
8323
- ...infraHandlers
8529
+ ...infraHandlers,
8530
+ ...qaRecordingsHandlers
8324
8531
  };
8325
8532
  var GROUP_HELP = {
8326
8533
  requests: requestsHelp,
@@ -8345,7 +8552,8 @@ var GROUP_HELP = {
8345
8552
  settings: settingsHelp,
8346
8553
  email: emailHelp,
8347
8554
  cron: cronHelp,
8348
- infra: infraHelp
8555
+ infra: infraHelp,
8556
+ "qa-recordings": qaRecordingsHelp
8349
8557
  };
8350
8558
 
8351
8559
  // src/ctx.ts
@@ -8405,29 +8613,30 @@ var HELP = `remy-admin \u2014 Manage your production Remy app.
8405
8613
  Usage: remy-admin <command> <subcommand> [options]
8406
8614
 
8407
8615
  Commands:
8408
- requests View request logs and metrics
8409
- crashes View frontend (browser) crash groups and events
8410
- analytics View traffic, top-N, geo, and AI-referral insights
8411
- releases View and monitor releases
8412
- diagnostics View the post-deploy Lighthouse audit (scores, issues, raw report)
8413
- domains Manage custom subdomain
8414
- users Manage app users and roles
8415
- db Query the production database
8416
- secrets Manage app secrets (env vars)
8417
- methods List and invoke methods
8418
- data Sync databases between dev and live (lift-from-dev / lift-from-live)
8419
- issues File and manage issues, in this app or another app in the workspace
8420
- prerender Manage + verify prerendered snapshots served to bots/crawlers
8421
- files Store, retrieve, share (signed links) + manage files in the app's stores
8422
- datasources Build and query searchable document corpora (RAG)
8423
- voice Phone numbers, voice call log + transcripts, voice policy settings
8424
- agent Agent (chat) conversation log + transcripts
8425
- events Realtime events \u2014 live tail, test publish, channel activity
8426
- jewels Monitor jewel shadowing; review + approve the proposal queue
8427
- settings App settings \u2014 signup allowlist, test accounts, embed origins, toggles
8428
- email Email \u2014 delivery log, blast stats, unsubscribes, inbox, custom domains
8429
- cron Scheduled jobs \u2014 schedules, run history, manual trigger
8430
- infra Dedicated infrastructure \u2014 provision, hibernate, resize + destroy retrieval capacity
8616
+ requests View request logs and metrics
8617
+ crashes View frontend (browser) crash groups and events
8618
+ analytics View traffic, top-N, geo, and AI-referral insights
8619
+ releases View and monitor releases
8620
+ diagnostics View the post-deploy Lighthouse audit (scores, issues, raw report)
8621
+ domains Manage custom subdomain
8622
+ users Manage app users and roles
8623
+ db Query the production database
8624
+ secrets Manage app secrets (env vars)
8625
+ methods List and invoke methods
8626
+ data Sync databases between dev and live (lift-from-dev / lift-from-live)
8627
+ issues File and manage issues, in this app or another app in the workspace
8628
+ prerender Manage + verify prerendered snapshots served to bots/crawlers
8629
+ files Store, retrieve, share (signed links) + manage files in the app's stores
8630
+ datasources Build and query searchable document corpora (RAG)
8631
+ voice Phone numbers, voice call log + transcripts, voice policy settings
8632
+ agent Agent (chat) conversation log + transcripts
8633
+ events Realtime events \u2014 live tail, test publish, channel activity
8634
+ jewels Monitor jewel shadowing; review + approve the proposal queue
8635
+ settings App settings \u2014 signup allowlist, test accounts, embed origins, toggles
8636
+ email Email \u2014 delivery log, blast stats, unsubscribes, inbox, custom domains
8637
+ cron Scheduled jobs \u2014 schedules, run history, manual trigger
8638
+ infra Dedicated infrastructure \u2014 provision, hibernate, resize + destroy retrieval capacity
8639
+ qa-recordings Browser-QA session replays \u2014 list them, render one as an mp4
8431
8640
 
8432
8641
  Run 'remy-admin <command> --help' for details on each command.
8433
8642
 
@@ -8460,7 +8669,8 @@ var GROUPS = [
8460
8669
  "settings",
8461
8670
  "email",
8462
8671
  "cron",
8463
- "infra"
8672
+ "infra",
8673
+ "qa-recordings"
8464
8674
  ];
8465
8675
  var KEYS = new Set(Object.keys(SPECS));
8466
8676
  function resolveRoute(argv) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@madewithremy/admin",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Admin SDK + CLI for managing production Remy apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,9 +21,10 @@
21
21
  "scripts": {
22
22
  "build": "tsup",
23
23
  "dev": "tsup --watch",
24
- "typecheck": "tsc --noEmit",
25
- "lint:fix": "prettier --write ./src",
26
- "prepublishOnly": "npm run build"
24
+ "typecheck": "npm run tsc -- --noEmit",
25
+ "prepublishOnly": "npm run build",
26
+ "tsc": "node node_modules/@typescript/native/bin/tsc",
27
+ "//tsc": "The native TypeScript 7 compiler by explicit path. It (typescript@7 under the @typescript/native alias) and the JS compiler API behind the `typescript` alias — which tsup's dts step needs — both declare a `tsc` bin, so node_modules/.bin/tsc is whichever npm linked last."
27
28
  },
28
29
  "engines": {
29
30
  "node": ">=22.0.0"
@@ -53,9 +54,9 @@
53
54
  },
54
55
  "devDependencies": {
55
56
  "@types/node": "^25.3.2",
56
- "prettier": "^3.8.1",
57
+ "@typescript/native": "npm:typescript@^7.0.2",
57
58
  "tsup": "^8",
58
59
  "tsx": "^4",
59
- "typescript": "^5.5"
60
+ "typescript": "npm:@typescript/typescript6@^6.0.2"
60
61
  }
61
62
  }