@rebasepro/cli 0.13.0 → 0.13.1-canary.g394d868

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/index.es.js CHANGED
@@ -12,8 +12,9 @@ import crypto from "crypto";
12
12
  import { execSync, spawn, spawnSync } from "child_process";
13
13
  import os from "os";
14
14
  import { createRebaseClient } from "@rebasepro/client";
15
+ import dotenv from "dotenv";
15
16
  import { createRequire } from "module";
16
- import { BUNDLE_FORMAT_VERSION, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, normalizeStorageSources, storageEnvSuffix } from "@rebasepro/types";
17
+ import { BUNDLE_FORMAT_VERSION, DEFAULT_DATA_SOURCE_KEY, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, getDataSourceCapabilities, normalizeStorageSources, storageEnvSuffix } from "@rebasepro/types";
17
18
  import { generateSDK } from "@rebasepro/codegen";
18
19
  //#region src/utils/package-manager.ts
19
20
  /**
@@ -289,6 +290,34 @@ function findEnvFile(projectRoot) {
289
290
  return null;
290
291
  }
291
292
  /**
293
+ * Read the project's `.env` into a plain object.
294
+ *
295
+ * One reader, because there were four: `dotenv` in `start`, a hand-rolled
296
+ * `indexOf("=")` loop in `api-keys`, a single-key regex in `auth`, and its own
297
+ * splitting in `cloud env`. `dotenv` is a declared dependency of this package,
298
+ * so the other three existed for no reason and disagreed with the correct one
299
+ * on the two things people actually write in a `.env`:
300
+ *
301
+ * - `export KEY=value`, which the hand-rolled parser keyed as
302
+ * `export KEY` — so the command reported the key as unset while it was
303
+ * right there in the file;
304
+ * - `KEY=value # comment`, whose comment travelled into the value and then
305
+ * into an `Authorization` header, coming back as a 401 with nothing
306
+ * pointing at the cause.
307
+ *
308
+ * Returns `{}` when the project has no `.env`, so callers can treat "absent"
309
+ * and "empty" alike.
310
+ */
311
+ function readEnvFile(projectRoot) {
312
+ const envFile = findEnvFile(projectRoot);
313
+ if (!envFile || !fs.existsSync(envFile)) return {};
314
+ try {
315
+ return dotenv.parse(fs.readFileSync(envFile, "utf-8"));
316
+ } catch {
317
+ return {};
318
+ }
319
+ }
320
+ /**
292
321
  * Resolve a binary from the project's node_modules/.bin.
293
322
  * Checks backend, root, parent monorepo root, then falls back to PATH.
294
323
  */
@@ -1789,7 +1818,7 @@ async function createProject$1(options) {
1789
1818
  console.log(` ${chalk.cyan(runDev.join(" "))}`);
1790
1819
  }
1791
1820
  console.log("");
1792
- console.log(isBaas ? chalk.gray("This starts a headless API (Hono + PostgreSQL). There are no collection files: ") + chalk.gray("the API is derived from your database schema. Once it serves a table, docs are at /api/swagger.") : chalk.gray("This starts both the backend (Hono + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
1821
+ console.log(isBaas ? introspected ? chalk.gray("This starts a headless API (Hono + PostgreSQL) over the collections just ") + chalk.gray("generated from your database in config/collections. Edit them to change what the ") + chalk.gray("API exposes; docs are at /api/swagger.") : chalk.gray("This starts a headless API (Hono + PostgreSQL). There are no collection files: ") + chalk.gray("the API is derived from your database schema. Once it serves a table, docs are at /api/swagger.") : chalk.gray("This starts both the backend (Hono + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
1793
1822
  console.log("");
1794
1823
  console.log(chalk.gray("Docs: https://rebase.pro/docs"));
1795
1824
  console.log(chalk.gray("GitHub: https://github.com/rebasepro/rebase"));
@@ -2013,6 +2042,32 @@ function readCliVersion() {
2013
2042
  } catch {}
2014
2043
  return "latest";
2015
2044
  }
2045
+ /**
2046
+ * The runtime image tag to pin, given the version of the CLI doing the scaffolding.
2047
+ *
2048
+ * Only a stable release publishes `rebasepro/server` — a multi-arch build on
2049
+ * every push to main would cost minutes per commit for an image nobody pulls.
2050
+ * So pinning a prerelease CLI's own version writes a tag that cannot exist, and
2051
+ * `docker compose up` fails on `manifest unknown`, which is the same dead end
2052
+ * as the missing-repository bug this pinning was added to prevent.
2053
+ *
2054
+ * A prerelease therefore falls back to `latest`, which is correct rather than
2055
+ * merely available: a bundle's manifest declares the runtime range it needs
2056
+ * (`^1`), the image supplies only `@rebasepro/server`, and the framework a
2057
+ * bundle runs is installed from its own `deps.declared` at boot. The current
2058
+ * stable runtime boots a canary bundle by design.
2059
+ *
2060
+ * A floating tag is a real cost — it is what pinning exists to avoid — so say
2061
+ * so in the file rather than leaving a reader to discover it.
2062
+ */
2063
+ function resolveRuntimeImageTag(cliVersion) {
2064
+ if (/^\d+\.\d+\.\d+-/.test(cliVersion)) return {
2065
+ tag: "latest",
2066
+ note: `# Scaffolded by a prerelease CLI (${cliVersion}), which publishes no runtime image,\n# so this floats to the newest stable runtime. Pin an exact version once you
2067
+ # deploy: a moving tag changes what you are running with no version changing.`
2068
+ };
2069
+ return { tag: cliVersion };
2070
+ }
2016
2071
  async function configureEnvFile(targetDirectory, databaseUrl) {
2017
2072
  const envExamplePath = path.join(targetDirectory, ".env.example");
2018
2073
  const envPath = path.join(targetDirectory, ".env");
@@ -2031,8 +2086,9 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
2031
2086
  envContent = envContent.replace(/^#\s*REBASE_SERVICE_KEY=.*$/m, `REBASE_SERVICE_KEY=${serviceKey}`);
2032
2087
  const composeApiPort = /^PORT=(\d+)/m.exec(envContent)?.[1] ?? "3001";
2033
2088
  envContent = envContent.replace(/^#\s*CORS_ORIGINS=.*$/m, `CORS_ORIGINS=http://localhost:${composeApiPort}`);
2034
- const runtimeVersion = readCliVersion();
2035
- envContent = /^#?\s*REBASE_VERSION=.*$/m.test(envContent) ? envContent.replace(/^#?\s*REBASE_VERSION=.*$/m, `REBASE_VERSION=${runtimeVersion}`) : `${envContent.trimEnd()}\n\n# The Rebase runtime image tag docker-compose.yml pulls.\n# Change this and restart to upgrade; your project bundle is untouched.\nREBASE_VERSION=${runtimeVersion}\n`;
2089
+ const { tag: runtimeVersion, note } = resolveRuntimeImageTag(readCliVersion());
2090
+ const pinned = `${note ? `${note}\n` : ""}REBASE_VERSION=${runtimeVersion}`;
2091
+ envContent = /^#?\s*REBASE_VERSION=.*$/m.test(envContent) ? envContent.replace(/^#?\s*REBASE_VERSION=.*$/m, pinned) : `${envContent.trimEnd()}\n\n# The Rebase runtime image tag docker-compose.yml pulls.\n# Change this and restart to upgrade; your project bundle is untouched.\n${pinned}\n`;
2036
2092
  if (databaseUrl) {
2037
2093
  if (/[\r\n]/.test(databaseUrl)) throw new Error("Invalid DATABASE_URL: multiline values are not allowed.");
2038
2094
  const { pinSearchPath } = await import("@rebasepro/server-postgres");
@@ -2367,7 +2423,7 @@ async function schemaCommand(subcommand, rawArgs) {
2367
2423
  return;
2368
2424
  }
2369
2425
  const projectRoot = requireProjectRoot();
2370
- recordEvent("cli.schema_generate", { subcommand: subcommand ?? "none" }, { projectRoot });
2426
+ recordEvent("cli.schema", { subcommand: subcommand ?? "none" }, { projectRoot });
2371
2427
  const backendDir = requireBackendDir(projectRoot);
2372
2428
  const activePlugin = getActiveBackendPlugin(backendDir);
2373
2429
  if (!activePlugin) {
@@ -2436,7 +2492,7 @@ async function dbCommand(subcommand, rawArgs) {
2436
2492
  return;
2437
2493
  }
2438
2494
  const projectRoot = requireProjectRoot();
2439
- recordEvent("cli.db_push", { subcommand: subcommand ?? "none" }, { projectRoot });
2495
+ recordEvent("cli.db", { subcommand: subcommand ?? "none" }, { projectRoot });
2440
2496
  const backendDir = requireBackendDir(projectRoot);
2441
2497
  const activePlugin = getActiveBackendPlugin(backendDir);
2442
2498
  if (!activePlugin) {
@@ -3074,6 +3130,106 @@ function resolveBackendPaths(app, projectRoot) {
3074
3130
  };
3075
3131
  }
3076
3132
  //#endregion
3133
+ //#region src/utils/collection-drift.ts
3134
+ /**
3135
+ * Which edits under `config/collections` can put the *SQL* schema out of date.
3136
+ *
3137
+ * `rebase dev` watches that directory and, on any change, either tells the
3138
+ * developer to run `rebase schema generate` / `rebase db push` or runs them.
3139
+ * The watcher is recursive and knows nothing about what it is watching, so it
3140
+ * said that about every file under the directory — including a
3141
+ * `collections/firestore/exercises.ts`, whose documents live in Firestore and
3142
+ * for which there is no Drizzle schema to regenerate and no database to push
3143
+ * to. Advice that is wrong on every edit is advice a developer learns to
3144
+ * ignore, which is worse than none: the same box is the only warning for the
3145
+ * Postgres collection next to it, where it is real.
3146
+ *
3147
+ * Two independent reasons a change cannot affect the SQL schema, both checked
3148
+ * here:
3149
+ *
3150
+ * 1. **The loader would never read the file.** `loadCollectionsFromDirectory`
3151
+ * reads the top level of the collections directory only — no recursion, no
3152
+ * `index`, no tests, no declarations. A file it does not read cannot change
3153
+ * what it returns, and the watcher must not claim otherwise.
3154
+ * 2. **Every collection in it is served by another engine.** A Firestore or
3155
+ * MongoDB collection has no table, no migration and no policies.
3156
+ *
3157
+ * The engine check reads the source text rather than importing the module: the
3158
+ * CLI runs as plain Node and cannot evaluate a project's TypeScript, and a
3159
+ * watcher must answer in the time between two keystrokes. Anything it cannot
3160
+ * read confidently counts as SQL-affecting — a spurious warning is a nuisance,
3161
+ * a suppressed one hides real drift.
3162
+ */
3163
+ /**
3164
+ * Would `loadCollectionsFromDirectory` load this file?
3165
+ *
3166
+ * Mirrors that loader's own `isCollectionFile` plus its flat (non-recursive)
3167
+ * scan. `relativePath` is relative to the collections directory, as `fs.watch`
3168
+ * reports it.
3169
+ */
3170
+ function isLoadedCollectionFile(relativePath) {
3171
+ const normalized = relativePath.split(path.sep).join("/");
3172
+ if (normalized.includes("/")) return false;
3173
+ const file = normalized;
3174
+ if (!file.endsWith(".ts") && !file.endsWith(".js")) return false;
3175
+ if (file.startsWith(".")) return false;
3176
+ if (file.includes(".test.")) return false;
3177
+ if (file.endsWith(".d.ts")) return false;
3178
+ if (file === "index.ts" || file === "index.js") return false;
3179
+ return true;
3180
+ }
3181
+ var ENGINE_LITERAL = /\bengine\s*:\s*["'`]([^"'`]+)["'`]/g;
3182
+ var DATA_SOURCE_LITERAL = /\bdataSource\s*:\s*["'`]([^"'`]+)["'`]/g;
3183
+ /**
3184
+ * Drop comments, so a `// engine: "firestore"` in a docblock cannot silence a
3185
+ * warning about the Postgres collection the file actually declares.
3186
+ *
3187
+ * Deliberately naive — it does not understand strings, so a `//` inside one
3188
+ * (a URL in a default value) eats the rest of that line. That only ever removes
3189
+ * text, and removing an engine literal makes this file *more* likely to warn,
3190
+ * which is the side to be wrong on.
3191
+ */
3192
+ function stripComments(source) {
3193
+ return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
3194
+ }
3195
+ function literals(source, pattern) {
3196
+ const found = [];
3197
+ for (const match of source.matchAll(pattern)) found.push(match[1]);
3198
+ return found;
3199
+ }
3200
+ /**
3201
+ * Does this collection source declare anything a SQL toolchain would own?
3202
+ *
3203
+ * The fallback order matches `resolveDataSource`: an explicit `engine` wins,
3204
+ * and a collection that only names a `dataSource` is resolved as if the key
3205
+ * were the engine — which is what that function does when no registry is
3206
+ * available, and the CLI has none. An engine nobody recognises counts as
3207
+ * relational, for the reason `isRelationalCollection` gives.
3208
+ *
3209
+ * A file declaring several collections is SQL-affecting if *any* of them is.
3210
+ */
3211
+ function declaresRelationalCollection(rawSource) {
3212
+ const source = stripComments(rawSource);
3213
+ const engines = literals(source, ENGINE_LITERAL);
3214
+ const declared = engines.length > 0 ? engines : literals(source, DATA_SOURCE_LITERAL).filter((key) => key !== DEFAULT_DATA_SOURCE_KEY);
3215
+ if (declared.length === 0) return true;
3216
+ return declared.some((engine) => getDataSourceCapabilities(engine).supportsRelations);
3217
+ }
3218
+ /**
3219
+ * Can this edit have changed the generated SQL schema?
3220
+ *
3221
+ * Answers `true` when it cannot tell — an unreadable file is a reason to warn,
3222
+ * not a reason to go quiet.
3223
+ */
3224
+ function affectsSqlSchema(collectionsDir, relativePath) {
3225
+ if (!isLoadedCollectionFile(relativePath)) return false;
3226
+ try {
3227
+ return declaresRelationalCollection(fs.readFileSync(path.join(collectionsDir, relativePath), "utf8"));
3228
+ } catch {
3229
+ return true;
3230
+ }
3231
+ }
3232
+ //#endregion
3077
3233
  //#region src/commands/dev.ts
3078
3234
  /**
3079
3235
  * CLI command: rebase dev
@@ -3188,7 +3344,7 @@ async function devCommand(rawArgs) {
3188
3344
  "--help": Boolean,
3189
3345
  "-b": "--backend-only",
3190
3346
  "-f": "--frontend-only",
3191
- "-p": "--port",
3347
+ "-P": "--port",
3192
3348
  "-g": "--generate",
3193
3349
  "-h": "--help"
3194
3350
  }, {
@@ -3345,6 +3501,20 @@ async function devCommand(rawArgs) {
3345
3501
  } catch {}
3346
3502
  /** Whether the frontend has been launched (we only launch it once). */
3347
3503
  let frontendLaunched = false;
3504
+ try {
3505
+ const activePlugin = getActiveBackendPlugin(backendDir);
3506
+ const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
3507
+ if (pluginCli) await execa(tsxBin, [
3508
+ pluginCli,
3509
+ "schema",
3510
+ "stale",
3511
+ "--fix"
3512
+ ], {
3513
+ cwd: backendDir,
3514
+ stdio: "inherit",
3515
+ env
3516
+ });
3517
+ } catch {}
3348
3518
  if (shouldGenerate) {
3349
3519
  console.log(chalk.gray(" → Ensuring schema and SDK are generated on start..."));
3350
3520
  try {
@@ -3372,14 +3542,18 @@ async function devCommand(rawArgs) {
3372
3542
  const collectionsDir = path.join(projectRoot, "config", "collections");
3373
3543
  if (fs.existsSync(collectionsDir)) {
3374
3544
  let watchDebounce = null;
3545
+ let sqlSchemaAffected = false;
3375
3546
  fs.watch(collectionsDir, { recursive: true }, (eventType, filename) => {
3376
3547
  if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
3548
+ sqlSchemaAffected = sqlSchemaAffected || affectsSqlSchema(collectionsDir, filename);
3377
3549
  if (watchDebounce) clearTimeout(watchDebounce);
3378
3550
  watchDebounce = setTimeout(async () => {
3379
- console.log(chalk.yellow(`\n 🔄 Collection change detected (${filename}). Regenerating schema & SDK...`));
3551
+ const regenerateSchema = sqlSchemaAffected;
3552
+ sqlSchemaAffected = false;
3553
+ console.log(chalk.yellow(`\n 🔄 Collection change detected (${filename}). Regenerating ${regenerateSchema ? "schema & SDK" : "SDK"}...`));
3380
3554
  try {
3381
3555
  const activePlugin = getActiveBackendPlugin(backendDir);
3382
- const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
3556
+ const pluginCli = regenerateSchema && activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
3383
3557
  if (pluginCli) await execa(tsxBin, [
3384
3558
  pluginCli,
3385
3559
  "schema",
@@ -3395,7 +3569,7 @@ async function devCommand(rawArgs) {
3395
3569
  stdio: "inherit",
3396
3570
  env
3397
3571
  });
3398
- console.log(chalk.green(" ✓ Schema & SDK regenerated successfully. Hono will reload."));
3572
+ console.log(chalk.green(`${regenerateSchema ? "Schema & SDK" : "SDK"} regenerated successfully. Hono will reload.`));
3399
3573
  } catch (err) {
3400
3574
  console.error(chalk.red(` ✗ Failed to regenerate schema/SDK: ${err instanceof Error ? err.message : err}`));
3401
3575
  }
@@ -3420,12 +3594,14 @@ async function devCommand(rawArgs) {
3420
3594
  let driftDebounce = null;
3421
3595
  fs.watch(collectionsDir, { recursive: true }, (_eventType, filename) => {
3422
3596
  if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
3597
+ if (!affectsSqlSchema(collectionsDir, filename)) return;
3423
3598
  if (driftDebounce) clearTimeout(driftDebounce);
3424
3599
  driftDebounce = setTimeout(() => {
3600
+ const shown = filename.length > 31 ? `…${filename.slice(-30)}` : filename.padEnd(31);
3425
3601
  console.log([
3426
3602
  "",
3427
3603
  chalk.yellow(" ┌──────────────────────────────────────────────────────────────┐"),
3428
- chalk.yellow(" │ ⚠️ Collection file changed: ") + chalk.white(filename.padEnd(31)) + chalk.yellow("│"),
3604
+ chalk.yellow(" │ ⚠️ Collection file changed: ") + chalk.white(shown) + chalk.yellow("│"),
3429
3605
  chalk.yellow(" │ │"),
3430
3606
  chalk.yellow(" │ Your schema may be out of sync. Run: │"),
3431
3607
  chalk.yellow(" │ ") + chalk.cyan("rebase schema generate") + chalk.yellow(" regenerate Drizzle schema │"),
@@ -4732,7 +4908,8 @@ ${chalk.bold("Examples")}
4732
4908
  }
4733
4909
  async function buildCommand(rawArgs = []) {
4734
4910
  const args = arg({
4735
- "--out": String,
4911
+ "--output": String,
4912
+ "--out": "--output",
4736
4913
  "--skip-type-check": Boolean,
4737
4914
  "--skip-schema": Boolean,
4738
4915
  "--no-static": Boolean,
@@ -4800,7 +4977,7 @@ async function buildCommand(rawArgs = []) {
4800
4977
  projectRoot,
4801
4978
  appName: name,
4802
4979
  app,
4803
- outDir: args["--out"],
4980
+ outDir: args["--output"],
4804
4981
  runtimeRange: manifest.rebase,
4805
4982
  storage: manifest.storage,
4806
4983
  skipTypeCheck: args["--skip-type-check"],
@@ -4837,7 +5014,7 @@ async function buildCommand(rawArgs = []) {
4837
5014
  });
4838
5015
  for (const outcome of folded ?? []) console.log(chalk.green(` ✓ ${outcome.appName} folded in`) + chalk.dim(` (${outcome.fileCount} file(s) → served at ${outcome.path})`));
4839
5016
  }
4840
- } else if (app.type === "static") await buildAssetApp(projectRoot, name, app, manifest.rebase, args["--out"]);
5017
+ } else if (app.type === "static") await buildAssetApp(projectRoot, name, app, manifest.rebase, args["--output"]);
4841
5018
  console.log("");
4842
5019
  }
4843
5020
  console.log(chalk.green("✓ Build complete."));
@@ -5286,6 +5463,48 @@ async function startWorkspaceBackend(projectRoot, env) {
5286
5463
  * Subcommands:
5287
5464
  * reset-password — Reset a user's password
5288
5465
  */
5466
+ /**
5467
+ * Pick the user with exactly this email out of a search response.
5468
+ *
5469
+ * `/api/admin/users?search=` is an `ILIKE '%…%'` over email **or display
5470
+ * name**, ordered by role count descending. This used to take row `[0]` and
5471
+ * reset it, then print the email it had been *given* as confirmation — so two
5472
+ * ordinary situations ended in a successful-looking reset of somebody else's
5473
+ * account:
5474
+ *
5475
+ * - a substring collision: `bob@example.com` also matches
5476
+ * `robert.bob@example.com`;
5477
+ * - a display name, which is user-controlled and accepted up to 255
5478
+ * characters with no constraint on its content, containing an address
5479
+ * belonging to someone else.
5480
+ *
5481
+ * The ordering makes it worse rather than better — `array_length(roles) DESC
5482
+ * NULLS LAST` puts the most privileged match first, so the account most likely
5483
+ * to be reset by mistake is an admin's.
5484
+ *
5485
+ * Returns `undefined` when nothing matched exactly, which the caller reports
5486
+ * rather than falling through to a guess. The direct-database fallback below
5487
+ * has always matched with `eq(usersTable.email, email)`; this is the same
5488
+ * definition, so the command no longer resets different accounts depending on
5489
+ * whether the backend happened to be running.
5490
+ */
5491
+ function selectUserForEmail(payload, email) {
5492
+ const wanted = email.trim().toLowerCase();
5493
+ if (!wanted) return void 0;
5494
+ const rows = Array.isArray(payload) ? payload : payload && typeof payload === "object" && Array.isArray(payload.users) ? payload.users : [];
5495
+ for (const row of rows) {
5496
+ if (!row || typeof row !== "object") continue;
5497
+ const record = row;
5498
+ const rowEmail = typeof record.email === "string" ? record.email.trim().toLowerCase() : void 0;
5499
+ if (!rowEmail || rowEmail !== wanted) continue;
5500
+ const id = typeof record.id === "string" ? record.id : typeof record.uid === "string" ? record.uid : void 0;
5501
+ if (!id) continue;
5502
+ return {
5503
+ id,
5504
+ email: record.email
5505
+ };
5506
+ }
5507
+ }
5289
5508
  async function authCommand(subcommand, rawArgs) {
5290
5509
  if (!subcommand || subcommand === "--help") {
5291
5510
  printAuthHelp();
@@ -5306,8 +5525,7 @@ async function resetPassword(rawArgs) {
5306
5525
  const args = arg({
5307
5526
  "--email": String,
5308
5527
  "--password": String,
5309
- "-e": "--email",
5310
- "-p": "--password"
5528
+ "-e": "--email"
5311
5529
  }, {
5312
5530
  argv: rawArgs.slice(4),
5313
5531
  permissive: true
@@ -5322,12 +5540,8 @@ async function resetPassword(rawArgs) {
5322
5540
  process.exit(1);
5323
5541
  }
5324
5542
  const projectRoot = requireProjectRoot();
5325
- let envServiceKey;
5326
5543
  const envFile = findEnvFile(projectRoot);
5327
- if (envFile && fs.existsSync(envFile)) try {
5328
- const match = fs.readFileSync(envFile, "utf8").match(/^\s*REBASE_SERVICE_KEY\s*=\s*['"]?(.*?)['"]?\s*$/m);
5329
- if (match && match[1]) envServiceKey = match[1];
5330
- } catch {}
5544
+ const envServiceKey = readEnvFile(projectRoot).REBASE_SERVICE_KEY;
5331
5545
  let baseUrl = process.env.REBASE_BASE_URL;
5332
5546
  let serviceKey = process.env.REBASE_SERVICE_KEY || envServiceKey;
5333
5547
  const statePath = path.join(projectRoot, ".rebase", "state.json");
@@ -5347,7 +5561,7 @@ async function resetPassword(rawArgs) {
5347
5561
  try {
5348
5562
  const finalPass = newPassword || "NewPassword123!";
5349
5563
  const cleanBaseUrl = baseUrl.replace(/\/+$/, "");
5350
- const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=1`;
5564
+ const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=50`;
5351
5565
  const searchRes = await fetch(searchUrl, { headers: {
5352
5566
  "Authorization": `Bearer ${serviceKey}`,
5353
5567
  "Accept": "application/json"
@@ -5355,18 +5569,9 @@ async function resetPassword(rawArgs) {
5355
5569
  if (!searchRes.ok) throw new Error(`Failed to list users: ${searchRes.statusText}`);
5356
5570
  const searchData = await searchRes.json();
5357
5571
  if (!searchData || typeof searchData !== "object") throw new Error("Invalid response format from user search API.");
5358
- let userId;
5359
- if (Array.isArray(searchData)) {
5360
- const firstUser = searchData[0];
5361
- if (firstUser && typeof firstUser === "object" && "id" in firstUser && typeof firstUser.id === "string") userId = firstUser.id;
5362
- else if (firstUser && typeof firstUser === "object" && "uid" in firstUser && typeof firstUser.uid === "string") userId = firstUser.uid;
5363
- } else if ("users" in searchData && Array.isArray(searchData.users)) {
5364
- const firstUser = searchData.users[0];
5365
- if (firstUser && typeof firstUser === "object" && "id" in firstUser && typeof firstUser.id === "string") userId = firstUser.id;
5366
- else if (firstUser && typeof firstUser === "object" && "uid" in firstUser && typeof firstUser.uid === "string") userId = firstUser.uid;
5367
- }
5368
- if (!userId) throw new Error(`User not found with email: ${email}`);
5369
- const resetUrl = `${cleanBaseUrl}/api/admin/users/${userId}/reset-password`;
5572
+ const matched = selectUserForEmail(searchData, email);
5573
+ if (!matched) throw new Error(`No user has the email ${email}.`);
5574
+ const resetUrl = `${cleanBaseUrl}/api/admin/users/${matched.id}/reset-password`;
5370
5575
  const resetRes = await fetch(resetUrl, {
5371
5576
  method: "POST",
5372
5577
  headers: {
@@ -5383,7 +5588,7 @@ async function resetPassword(rawArgs) {
5383
5588
  console.log("API reset successful.");
5384
5589
  console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (via API)"));
5385
5590
  console.log("");
5386
- console.log(` ${chalk.gray("Email:")} ${email}`);
5591
+ console.log(` ${chalk.gray("Email:")} ${matched.email}`);
5387
5592
  console.log(` ${chalk.gray("Password:")} ${finalPass}`);
5388
5593
  console.log("");
5389
5594
  return;
@@ -5451,10 +5656,12 @@ async function resetPassword() {
5451
5656
  if (result.length > 0) {
5452
5657
  console.log("✅ Password reset for: " + result[0].email);
5453
5658
  ${!newPassword ? "console.log(\" New password: \" + newPassword);" : ""}
5454
- } else {
5455
- console.log("✗ User not found: " + email);
5659
+ process.exit(0);
5456
5660
  }
5457
- process.exit(0);
5661
+ // Nothing was updated, so nothing was reset. Exiting 0 here reported
5662
+ // success for a no-op, which is what a script would have believed.
5663
+ console.error("✗ User not found: " + email);
5664
+ process.exit(1);
5458
5665
  }
5459
5666
 
5460
5667
  resetPassword().catch(console.error);
@@ -5472,11 +5679,20 @@ resetPassword().catch(console.error);
5472
5679
  stdio: "inherit",
5473
5680
  env
5474
5681
  });
5682
+ const cleanup = () => {
5683
+ try {
5684
+ fs.unlinkSync(tmpScriptPath);
5685
+ } catch {}
5686
+ };
5475
5687
  return new Promise((resolve) => {
5688
+ child.on("error", (err) => {
5689
+ cleanup();
5690
+ console.error(chalk.red("✗ Could not run the reset script."));
5691
+ console.error(chalk.gray(` ${err.message}`));
5692
+ process.exit(1);
5693
+ });
5476
5694
  child.on("close", (code) => {
5477
- try {
5478
- fs.unlinkSync(tmpScriptPath);
5479
- } catch {}
5695
+ cleanup();
5480
5696
  if (code !== 0) process.exit(code ?? 1);
5481
5697
  resolve();
5482
5698
  });
@@ -5514,7 +5730,34 @@ ${chalk.green.bold("Examples")}
5514
5730
  * Detects three-way schema drift between collection definitions,
5515
5731
  * the generated Drizzle schema, and the live PostgreSQL database.
5516
5732
  */
5733
+ /**
5734
+ * `--help` is answered before the project guard, not after.
5735
+ *
5736
+ * `doctor` declared no `--help` at all, so the flag fell through to the command
5737
+ * body and hit `requireProjectRoot()` — and `rebase doctor --help` outside a
5738
+ * project answered "✗ Could not find a Rebase project root." Asking a command
5739
+ * what it does is the one question that cannot require being somewhere
5740
+ * particular to ask.
5741
+ */
5742
+ function printDoctorHelp() {
5743
+ console.log(`
5744
+ ${chalk.bold("rebase doctor")} — Detect drift between collections, schema and database
5745
+
5746
+ ${chalk.green.bold("Usage")}
5747
+ rebase doctor
5748
+
5749
+ Compares the collections you declare, the generated Drizzle schema, and the
5750
+ tables that actually exist, then reports what disagrees and how to reconcile it.
5751
+
5752
+ Run from inside a Rebase project — it reads the project's collections and
5753
+ connects to its database.
5754
+ `);
5755
+ }
5517
5756
  async function doctorCommand(rawArgs) {
5757
+ if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
5758
+ printDoctorHelp();
5759
+ return;
5760
+ }
5518
5761
  const projectRoot = requireProjectRoot();
5519
5762
  const backendDir = requireBackendDir(projectRoot);
5520
5763
  const activePlugin = getActiveBackendPlugin(backendDir);
@@ -5663,7 +5906,8 @@ async function skillsCommand(subcommand, rawArgs) {
5663
5906
  }
5664
5907
  /**
5665
5908
  * Agents named explicitly on the command line, e.g. `--agent claude --agent cursor`
5666
- * (also accepts a comma-separated list). Returns null when none were given.
5909
+ * (also accepts a comma-separated list, and `all`). Returns null when none were
5910
+ * given.
5667
5911
  */
5668
5912
  function parseAgentFlags(rawArgs) {
5669
5913
  const requested = [];
@@ -5673,6 +5917,7 @@ function parseAgentFlags(rawArgs) {
5673
5917
  if (value && !value.startsWith("-")) requested.push(...value.split(",").map((v) => v.trim()).filter(Boolean));
5674
5918
  }
5675
5919
  if (requested.length === 0) return null;
5920
+ if (requested.includes("all")) return Object.keys(AGENTS);
5676
5921
  const valid = Object.keys(AGENTS);
5677
5922
  const unknown = requested.filter((a) => !valid.includes(a));
5678
5923
  if (unknown.length > 0) {
@@ -5700,6 +5945,7 @@ async function skillsInstall(rawArgs = []) {
5700
5945
  if (!process.stdin.isTTY) {
5701
5946
  console.error(chalk.red("Cannot prompt: this is a non-interactive terminal (no TTY)."));
5702
5947
  console.error(chalk.yellow(` Name the agents explicitly, e.g. rebase skills install --agent ${Object.keys(AGENTS)[0]}`));
5948
+ console.error(chalk.yellow(" Or install for every supported agent: rebase skills install --agent all"));
5703
5949
  console.error(chalk.gray(` Available: ${Object.keys(AGENTS).join(", ")}`));
5704
5950
  process.exit(1);
5705
5951
  }
@@ -5747,13 +5993,16 @@ ${chalk.green.bold("Subcommands")}
5747
5993
 
5748
5994
  ${chalk.green.bold("Options")}
5749
5995
  ${chalk.blue("--agent, -a")} Agent(s) to install for, skipping detection and the prompt.
5750
- Repeat the flag or pass a comma-separated list.
5751
- Available: ${Object.keys(AGENTS).join(", ")}
5996
+ Repeat the flag or pass a comma-separated list, or ${chalk.bold("all")}.
5997
+ Required without a TTY: a scaffolded project carries a marker
5998
+ file for every agent, so detection cannot pick one for you.
5999
+ Available: ${Object.keys(AGENTS).join(", ")}, all
5752
6000
 
5753
6001
  ${chalk.green.bold("Examples")}
5754
6002
  ${chalk.cyan("rebase skills install")}
5755
6003
  ${chalk.cyan("rebase skills install --agent claude")}
5756
6004
  ${chalk.cyan("rebase skills install --agent claude,cursor")}
6005
+ ${chalk.cyan("rebase skills install --agent all")} ${chalk.gray("# scripted / CI")}
5757
6006
  `);
5758
6007
  }
5759
6008
  //#endregion
@@ -5766,25 +6015,13 @@ ${chalk.green.bold("Examples")}
5766
6015
  * create — Create a new API key
5767
6016
  * revoke — Revoke an existing API key
5768
6017
  */
5769
- function loadEnv(projectRoot) {
5770
- const envFile = findEnvFile(projectRoot);
5771
- const env = {};
5772
- if (envFile && fs.existsSync(envFile)) {
5773
- const content = fs.readFileSync(envFile, "utf-8");
5774
- for (const line of content.split("\n")) {
5775
- const trimmed = line.trim();
5776
- if (!trimmed || trimmed.startsWith("#")) continue;
5777
- const idx = trimmed.indexOf("=");
5778
- if (idx > 0) {
5779
- const key = trimmed.slice(0, idx).trim();
5780
- let value = trimmed.slice(idx + 1).trim();
5781
- if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
5782
- env[key] = value;
5783
- }
5784
- }
5785
- }
5786
- return env;
5787
- }
6018
+ /**
6019
+ * Was a hand-rolled `indexOf("=")` loop. It keyed `export KEY=value` as
6020
+ * `export KEY` and carried a trailing `# comment` into the value — so a key
6021
+ * that was present read as absent, or reached an `Authorization` header with a
6022
+ * comment attached and came back 401. See `readEnvFile`.
6023
+ */
6024
+ var loadEnv = readEnvFile;
5788
6025
  function resolveBaseUrl(env, projectRoot) {
5789
6026
  if (env.REBASE_BASE_URL) return env.REBASE_BASE_URL;
5790
6027
  if (projectRoot) try {
@@ -6055,7 +6292,12 @@ ${chalk.green.bold("Examples")}
6055
6292
  * a documentation comment that quietly fell out of date two releases ago.
6056
6293
  */
6057
6294
  async function telemetryCommand(rawArgs) {
6058
- switch (rawArgs.slice(3).filter((a) => !a.startsWith("-"))[0]) {
6295
+ const subcommand = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[0];
6296
+ if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
6297
+ printHelp$2();
6298
+ return;
6299
+ }
6300
+ switch (subcommand) {
6059
6301
  case "status":
6060
6302
  case void 0:
6061
6303
  printStatus();
@@ -6154,8 +6396,7 @@ async function loginCommand(rawArgs) {
6154
6396
  const args = arg({
6155
6397
  "--email": String,
6156
6398
  "--password": String,
6157
- "-e": "--email",
6158
- "-p": "--password"
6399
+ "-e": "--email"
6159
6400
  }, {
6160
6401
  argv: rawArgs.slice(3),
6161
6402
  permissive: true
@@ -8162,7 +8403,8 @@ async function revealEnv(rawArgs) {
8162
8403
  }
8163
8404
  async function pullEnv(rawArgs) {
8164
8405
  const args = arg({
8165
- "--out": String,
8406
+ "--output": String,
8407
+ "--out": "--output",
8166
8408
  "--yes": Boolean,
8167
8409
  "-y": "--yes",
8168
8410
  "--project": String,
@@ -8174,7 +8416,7 @@ async function pullEnv(rawArgs) {
8174
8416
  const { client } = await requireClient(rawArgs);
8175
8417
  const projectId = await requireProject(rawArgs, client);
8176
8418
  displayProjectRef(rawArgs);
8177
- const outPath = path.resolve(args["--out"] || ".env");
8419
+ const outPath = path.resolve(args["--output"] || ".env");
8178
8420
  try {
8179
8421
  const list = await fetchEnvVars(client, projectId);
8180
8422
  if (fs.existsSync(outPath)) await confirmDestructive({
@@ -10293,15 +10535,43 @@ function positionals(rawArgs) {
10293
10535
  while (i < rest.length && rest[i].startsWith("-")) i++;
10294
10536
  return rest.slice(i);
10295
10537
  }
10538
+ /**
10539
+ * The help page for each group, keyed by every alias the dispatch below accepts.
10540
+ *
10541
+ * Aliases are listed explicitly rather than normalised first, so a group that
10542
+ * gains one and forgets it here degrades to the index page — wrong, but a page.
10543
+ * `cloud-help.test.ts` asserts the two stay in step.
10544
+ *
10545
+ * A group absent from this map has no page of its own; the index lists it.
10546
+ */
10547
+ var GROUP_HELP = {
10548
+ env: printEnvHelp,
10549
+ domains: printDomainsHelp,
10550
+ domain: printDomainsHelp,
10551
+ extensions: printExtensionsHelp,
10552
+ extension: printExtensionsHelp,
10553
+ settings: printSettingsHelp,
10554
+ orgs: printOrgsHelp,
10555
+ org: printOrgsHelp,
10556
+ db: printDbHelp,
10557
+ database: printDbHelp,
10558
+ debug: printDebugHelp,
10559
+ storage: printStorageHelp
10560
+ };
10296
10561
  async function cloudCommand(subcommand, rawArgs) {
10297
10562
  initOutputMode(rawArgs);
10298
10563
  const pos = positionals(rawArgs);
10299
10564
  const group = pos[0] ?? (subcommand !== "--help" ? subcommand : void 0);
10565
+ const wantsHelp = rawArgs.includes("--help") || rawArgs.includes("-h");
10300
10566
  const action = pos[1];
10301
- if (!group || subcommand === "--help") {
10567
+ if (!group) {
10302
10568
  printCloudHelp();
10303
10569
  return;
10304
10570
  }
10571
+ if (wantsHelp) {
10572
+ (GROUP_HELP[group] ?? printCloudHelp)();
10573
+ return;
10574
+ }
10305
10575
  switch (group) {
10306
10576
  case "login":
10307
10577
  await loginCommand(rawArgs);
@@ -10741,7 +11011,7 @@ async function entry(args) {
10741
11011
  printHelp();
10742
11012
  return;
10743
11013
  }
10744
- const effectiveSubcommand = parsedArgs["--help"] ? "--help" : subcommand;
11014
+ const effectiveSubcommand = parsedArgs["--help"] && !subcommand ? "--help" : subcommand;
10745
11015
  switch (command) {
10746
11016
  case "init":
10747
11017
  await createRebaseApp(args);
@@ -10860,9 +11130,11 @@ ${chalk.green.bold("API Keys")}
10860
11130
  ${chalk.blue.bold("api-keys list")} List all service API keys
10861
11131
  ${chalk.blue.bold("api-keys create")} Create a new scoped API key
10862
11132
  ${chalk.blue.bold("api-keys revoke")} Revoke an existing API key
10863
- ${chalk.blue.bold("telemetry")} Anonymous usage sharing (opt-in, off by default)
10864
11133
  ${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
10865
11134
 
11135
+ ${chalk.green.bold("Usage sharing")}
11136
+ ${chalk.blue.bold("telemetry")} Anonymous usage sharing (opt-in, off by default)
11137
+
10866
11138
  ${chalk.green.bold("Rebase Cloud")}
10867
11139
  ${chalk.blue.bold("cloud login")} Sign in to the hosted control plane
10868
11140
  ${chalk.blue.bold("cloud link")} Link this directory to a cloud project
@@ -10893,6 +11165,6 @@ function telemetryNotice() {
10893
11165
  return chalk.gray(`Usage sharing: ${sharing ? "on" : "off"} — ${chalk.cyan("rebase telemetry")} to inspect or change\n`);
10894
11166
  }
10895
11167
  //#endregion
10896
- export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, DEV_PORT_FILENAME, MANIFEST_FILENAME, ManifestError, TEMPLATE_PLACEHOLDER_FILES, appsCommand, assessManagedCompatibility, authCommand, buildBundle, buildCommand, buildInitQuestions, buildStaticBundle, buildableApps, cloudCommand, collectDeclaredDependencies, configureEnvFile, createRebaseApp, dbCommand, detectFrameworkDepDrift, detectNativeDependencies, detectPackageManager, detectStorageAuthorize, devCommand, doctorCommand, ejectCommand, entry, findBackendApp, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, findUnusedServerEntry, foldStaticIntoBundle, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, getProjectPort, isIdentifierLike, isPnpmAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, positionals, printInitHelp, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveCliVersion, resolveExampleBaseUrl, resolveLocalBin, resolvePluginCliScript, resolveStartPort, resolveTsx, schemaCommand, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, writeManifest };
11168
+ export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, DEV_PORT_FILENAME, MANIFEST_FILENAME, ManifestError, TEMPLATE_PLACEHOLDER_FILES, appsCommand, assessManagedCompatibility, authCommand, buildBundle, buildCommand, buildInitQuestions, buildStaticBundle, buildableApps, cloudCommand, collectDeclaredDependencies, configureEnvFile, createRebaseApp, dbCommand, detectFrameworkDepDrift, detectNativeDependencies, detectPackageManager, detectStorageAuthorize, devCommand, doctorCommand, ejectCommand, entry, findBackendApp, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, findUnusedServerEntry, foldStaticIntoBundle, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, getProjectPort, isIdentifierLike, isPnpmAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, positionals, printInitHelp, readEnvFile, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveCliVersion, resolveExampleBaseUrl, resolveLocalBin, resolvePluginCliScript, resolveRuntimeImageTag, resolveStartPort, resolveTsx, schemaCommand, selectUserForEmail, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, writeManifest };
10897
11169
 
10898
11170
  //# sourceMappingURL=index.es.js.map