@rebasepro/cli 0.13.0 → 0.13.1-canary.g249daa1
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/commands/auth.d.ts +31 -0
- package/dist/commands/cloud/databases.d.ts +1 -0
- package/dist/commands/cloud/debug.d.ts +1 -0
- package/dist/commands/cloud/domains.d.ts +1 -0
- package/dist/commands/cloud/env.d.ts +1 -0
- package/dist/commands/cloud/extensions.d.ts +1 -0
- package/dist/commands/cloud/orgs.d.ts +1 -0
- package/dist/commands/cloud/resources.d.ts +1 -0
- package/dist/commands/cloud/settings.d.ts +1 -0
- package/dist/commands/dev.d.ts +0 -7
- package/dist/commands/init.d.ts +51 -0
- package/dist/fold-static.d.ts +47 -0
- package/dist/index.es.js +536 -116
- package/dist/index.es.js.map +1 -1
- package/dist/telemetry/payload.d.ts +1 -1
- package/dist/utils/collection-drift.d.ts +27 -0
- package/dist/utils/project.d.ts +20 -0
- package/package.json +7 -7
- package/templates/eject/docker-compose.custom.yml +4 -4
- package/templates/template/.env.example +13 -4
- package/templates/template/config/package.json +1 -0
- package/templates/template/docker-compose.yml +4 -4
package/dist/index.es.js
CHANGED
|
@@ -12,9 +12,10 @@ 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
|
|
16
|
-
import { BUNDLE_FORMAT_VERSION, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, normalizeStorageSources, storageEnvSuffix } from "@rebasepro/types";
|
|
15
|
+
import dotenv from "dotenv";
|
|
16
|
+
import { BUNDLE_FORMAT_VERSION, DEFAULT_DATA_SOURCE_KEY, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, getDataSourceCapabilities, normalizeStorageSources, storageEnvSuffix } from "@rebasepro/types";
|
|
17
17
|
import { generateSDK } from "@rebasepro/codegen";
|
|
18
|
+
import { createRequire } from "module";
|
|
18
19
|
//#region src/utils/package-manager.ts
|
|
19
20
|
/**
|
|
20
21
|
* Package manager detection and command abstraction.
|
|
@@ -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
|
*/
|
|
@@ -949,13 +978,35 @@ function sanitize(properties) {
|
|
|
949
978
|
}
|
|
950
979
|
return out;
|
|
951
980
|
}
|
|
981
|
+
/**
|
|
982
|
+
* The CLI's own version, read by walking up to this package's manifest.
|
|
983
|
+
*
|
|
984
|
+
* The obvious `require("../../package.json")` was wrong everywhere, not only on
|
|
985
|
+
* one install path: `vite build` bundles this module into `dist/index.es.js`, so
|
|
986
|
+
* the specifier resolves relative to `<pkg>/dist/` and lands on
|
|
987
|
+
* `<parent-of-pkg>/package.json` — a file that does not exist under npm, pnpm or
|
|
988
|
+
* the monorepo. Every event ever sent carried `cliVersion: "unknown"`, which is
|
|
989
|
+
* the one field that makes the rest of a payload interpretable.
|
|
990
|
+
*
|
|
991
|
+
* So walk up and check the manifest's `name` rather than counting directory
|
|
992
|
+
* levels: the count differs between `src/telemetry/` and the bundled `dist/`,
|
|
993
|
+
* and a wrong count fails silently by finding *some* package.json — the nearest
|
|
994
|
+
* dependency's, under a hoisted layout. Matching the name cannot do that.
|
|
995
|
+
*/
|
|
952
996
|
function cliVersion() {
|
|
953
997
|
try {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
998
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
999
|
+
const root = path.parse(dir).root;
|
|
1000
|
+
while (dir && dir !== root) {
|
|
1001
|
+
const manifest = path.join(dir, "package.json");
|
|
1002
|
+
if (fs.existsSync(manifest)) {
|
|
1003
|
+
const pkg = JSON.parse(fs.readFileSync(manifest, "utf-8"));
|
|
1004
|
+
if (pkg?.name === "@rebasepro/cli" && typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
1005
|
+
}
|
|
1006
|
+
dir = path.dirname(dir);
|
|
1007
|
+
}
|
|
1008
|
+
} catch {}
|
|
1009
|
+
return "unknown";
|
|
959
1010
|
}
|
|
960
1011
|
function buildEvent(event, properties, identity) {
|
|
961
1012
|
return {
|
|
@@ -1606,6 +1657,49 @@ async function linkScaffoldToCloud(options) {
|
|
|
1606
1657
|
console.warn(chalk.yellow(` ${linkLater}`));
|
|
1607
1658
|
}
|
|
1608
1659
|
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Make the initial commit, after everything that writes into the project has run.
|
|
1662
|
+
*
|
|
1663
|
+
* It used to happen immediately after `git init`, which is before dependency
|
|
1664
|
+
* installation and before introspection — so `init --git --install` ended on a
|
|
1665
|
+
* dirty tree whose only untracked file was `pnpm-lock.yaml`. A lockfile is
|
|
1666
|
+
* precisely the thing that should be in a project's first commit, and a brand
|
|
1667
|
+
* new scaffold whose first `git status` is dirty invites the reader to conclude
|
|
1668
|
+
* the lockfile is deliberately ignored and never commit it at all.
|
|
1669
|
+
*
|
|
1670
|
+
* Introspection has the same shape: it generates `config/collections` and
|
|
1671
|
+
* `schema.generated.ts`, which belong in the commit describing the scaffold that
|
|
1672
|
+
* produced them.
|
|
1673
|
+
*
|
|
1674
|
+
* `git init` stays where it was. Creating the repository early costs nothing and
|
|
1675
|
+
* means a failed install still leaves the user a repository to commit into.
|
|
1676
|
+
*/
|
|
1677
|
+
async function commitScaffold(targetDirectory) {
|
|
1678
|
+
try {
|
|
1679
|
+
await execa("git", ["add", "-A"], { cwd: targetDirectory });
|
|
1680
|
+
let identity = {};
|
|
1681
|
+
try {
|
|
1682
|
+
await execa("git", ["config", "user.email"], { cwd: targetDirectory });
|
|
1683
|
+
} catch {
|
|
1684
|
+
identity = {
|
|
1685
|
+
GIT_AUTHOR_NAME: "Rebase",
|
|
1686
|
+
GIT_AUTHOR_EMAIL: "noreply@rebase.pro",
|
|
1687
|
+
GIT_COMMITTER_NAME: "Rebase",
|
|
1688
|
+
GIT_COMMITTER_EMAIL: "noreply@rebase.pro"
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
await execa("git", [
|
|
1692
|
+
"commit",
|
|
1693
|
+
"-m",
|
|
1694
|
+
"Initial commit from Rebase"
|
|
1695
|
+
], {
|
|
1696
|
+
cwd: targetDirectory,
|
|
1697
|
+
env: identity
|
|
1698
|
+
});
|
|
1699
|
+
} catch {
|
|
1700
|
+
console.warn(chalk.yellow(" Warning: Failed to create the initial commit"));
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1609
1703
|
async function createProject$1(options) {
|
|
1610
1704
|
const startedAt = Date.now();
|
|
1611
1705
|
if (fs.existsSync(options.targetDirectory)) {
|
|
@@ -1645,6 +1739,7 @@ async function createProject$1(options) {
|
|
|
1645
1739
|
await applyHeadless(options.targetDirectory, options.headless);
|
|
1646
1740
|
await replacePlaceholders(options);
|
|
1647
1741
|
await configureEnvFile(options.targetDirectory, options.databaseUrl);
|
|
1742
|
+
let gitInitialized = false;
|
|
1648
1743
|
if (options.git) {
|
|
1649
1744
|
console.log(chalk.gray(" Initializing git repository..."));
|
|
1650
1745
|
try {
|
|
@@ -1656,26 +1751,7 @@ async function createProject$1(options) {
|
|
|
1656
1751
|
"refs/heads/main"
|
|
1657
1752
|
], { cwd: options.targetDirectory });
|
|
1658
1753
|
} catch {}
|
|
1659
|
-
|
|
1660
|
-
let identity = {};
|
|
1661
|
-
try {
|
|
1662
|
-
await execa("git", ["config", "user.email"], { cwd: options.targetDirectory });
|
|
1663
|
-
} catch {
|
|
1664
|
-
identity = {
|
|
1665
|
-
GIT_AUTHOR_NAME: "Rebase",
|
|
1666
|
-
GIT_AUTHOR_EMAIL: "noreply@rebase.pro",
|
|
1667
|
-
GIT_COMMITTER_NAME: "Rebase",
|
|
1668
|
-
GIT_COMMITTER_EMAIL: "noreply@rebase.pro"
|
|
1669
|
-
};
|
|
1670
|
-
}
|
|
1671
|
-
await execa("git", [
|
|
1672
|
-
"commit",
|
|
1673
|
-
"-m",
|
|
1674
|
-
"Initial commit from Rebase"
|
|
1675
|
-
], {
|
|
1676
|
-
cwd: options.targetDirectory,
|
|
1677
|
-
env: identity
|
|
1678
|
-
});
|
|
1754
|
+
gitInitialized = true;
|
|
1679
1755
|
} catch {
|
|
1680
1756
|
console.warn(chalk.yellow(" Warning: Failed to initialize git repository"));
|
|
1681
1757
|
}
|
|
@@ -1732,6 +1808,7 @@ async function createProject$1(options) {
|
|
|
1732
1808
|
console.warn(chalk.yellow(` Run \`${installCmd.join(" ")}\` then \`${execCmd.join(" ")}\` manually.`));
|
|
1733
1809
|
}
|
|
1734
1810
|
}
|
|
1811
|
+
if (gitInitialized) await commitScaffold(options.targetDirectory);
|
|
1735
1812
|
await linkScaffoldToCloud(options);
|
|
1736
1813
|
console.log("");
|
|
1737
1814
|
console.log(`${chalk.green.bold("✓")} Project ${chalk.bold(options.projectName)} created successfully!`);
|
|
@@ -1789,7 +1866,7 @@ async function createProject$1(options) {
|
|
|
1789
1866
|
console.log(` ${chalk.cyan(runDev.join(" "))}`);
|
|
1790
1867
|
}
|
|
1791
1868
|
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."));
|
|
1869
|
+
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
1870
|
console.log("");
|
|
1794
1871
|
console.log(chalk.gray("Docs: https://rebase.pro/docs"));
|
|
1795
1872
|
console.log(chalk.gray("GitHub: https://github.com/rebasepro/rebase"));
|
|
@@ -1976,18 +2053,51 @@ project directory ${path.basename(options.targetDirectory)}/ was created and is
|
|
|
1976
2053
|
fs.writeFileSync(fullPath, content, "utf-8");
|
|
1977
2054
|
}
|
|
1978
2055
|
}
|
|
1979
|
-
|
|
2056
|
+
/** `undefined` binds the wildcard address, which is a different question — see isPortAvailable. */
|
|
2057
|
+
function canBind(port, host) {
|
|
1980
2058
|
return new Promise((resolve) => {
|
|
1981
2059
|
const server = net.createServer();
|
|
1982
|
-
server.once("error", () => {
|
|
1983
|
-
resolve(
|
|
2060
|
+
server.once("error", (err) => {
|
|
2061
|
+
resolve(err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL");
|
|
1984
2062
|
});
|
|
1985
2063
|
server.once("listening", () => {
|
|
1986
2064
|
server.close(() => resolve(true));
|
|
1987
2065
|
});
|
|
1988
|
-
server.listen(port);
|
|
2066
|
+
if (host === void 0) server.listen(port);
|
|
2067
|
+
else server.listen(port, host);
|
|
1989
2068
|
});
|
|
1990
2069
|
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Whether `port` is free — on the wildcard address *and* on both loopback addresses.
|
|
2072
|
+
*
|
|
2073
|
+
* All three, because on macOS/BSD a successful bind does not mean the port is
|
|
2074
|
+
* unused. Sockets carry `SO_REUSEADDR` (Node sets it), under which a wildcard
|
|
2075
|
+
* bind and a specific-address bind on the same port do not conflict — in either
|
|
2076
|
+
* direction. So each probe alone has a blind spot, and they are different ones:
|
|
2077
|
+
*
|
|
2078
|
+
* - **Wildcard only** (what this used to do) misses a server bound to
|
|
2079
|
+
* `127.0.0.1` and `[::1]` — a Homebrew or Postgres.app install, i.e. most
|
|
2080
|
+
* developer machines. 5432 was reported free while it was already serving
|
|
2081
|
+
* another project's database. Docker then published `*:5432` for the same
|
|
2082
|
+
* reason and the container started cleanly, with no "port already allocated"
|
|
2083
|
+
* error anywhere to hint at the collision. `DATABASE_URL` pointed at
|
|
2084
|
+
* `localhost:5432`, `localhost` resolves to `::1` first, and every command
|
|
2085
|
+
* reported success while reading and writing the *pre-existing* database — a
|
|
2086
|
+
* `db push` would have created tables, roles and RLS policies inside it.
|
|
2087
|
+
*
|
|
2088
|
+
* - **Loopback only** misses the opposite case: Docker Desktop publishes a
|
|
2089
|
+
* container's port on `*`, and a specific-address bind succeeds right past it.
|
|
2090
|
+
* That port is free to probe and unusable to publish, so `docker compose up -d
|
|
2091
|
+
* db` fails on "Bind for 0.0.0.0:PORT failed: port is already allocated" —
|
|
2092
|
+
* loudly, but only after the project has been generated around the bad port.
|
|
2093
|
+
*
|
|
2094
|
+
* Requiring all three costs three sockets and leaves neither gap. The wildcard
|
|
2095
|
+
* bind is the one the container itself has to make; the loopback binds are the
|
|
2096
|
+
* addresses `DATABASE_URL` will actually name.
|
|
2097
|
+
*/
|
|
2098
|
+
async function isPortAvailable(port) {
|
|
2099
|
+
return await canBind(port) && await canBind(port, "127.0.0.1") && await canBind(port, "::1");
|
|
2100
|
+
}
|
|
1991
2101
|
async function findAvailablePort(startPort) {
|
|
1992
2102
|
let port = startPort;
|
|
1993
2103
|
while (!await isPortAvailable(port)) port++;
|
|
@@ -2013,6 +2123,32 @@ function readCliVersion() {
|
|
|
2013
2123
|
} catch {}
|
|
2014
2124
|
return "latest";
|
|
2015
2125
|
}
|
|
2126
|
+
/**
|
|
2127
|
+
* The runtime image tag to pin, given the version of the CLI doing the scaffolding.
|
|
2128
|
+
*
|
|
2129
|
+
* Only a stable release publishes `rebasepro/server` — a multi-arch build on
|
|
2130
|
+
* every push to main would cost minutes per commit for an image nobody pulls.
|
|
2131
|
+
* So pinning a prerelease CLI's own version writes a tag that cannot exist, and
|
|
2132
|
+
* `docker compose up` fails on `manifest unknown`, which is the same dead end
|
|
2133
|
+
* as the missing-repository bug this pinning was added to prevent.
|
|
2134
|
+
*
|
|
2135
|
+
* A prerelease therefore falls back to `latest`, which is correct rather than
|
|
2136
|
+
* merely available: a bundle's manifest declares the runtime range it needs
|
|
2137
|
+
* (`^1`), the image supplies only `@rebasepro/server`, and the framework a
|
|
2138
|
+
* bundle runs is installed from its own `deps.declared` at boot. The current
|
|
2139
|
+
* stable runtime boots a canary bundle by design.
|
|
2140
|
+
*
|
|
2141
|
+
* A floating tag is a real cost — it is what pinning exists to avoid — so say
|
|
2142
|
+
* so in the file rather than leaving a reader to discover it.
|
|
2143
|
+
*/
|
|
2144
|
+
function resolveRuntimeImageTag(cliVersion) {
|
|
2145
|
+
if (/^\d+\.\d+\.\d+-/.test(cliVersion)) return {
|
|
2146
|
+
tag: "latest",
|
|
2147
|
+
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
|
|
2148
|
+
# deploy: a moving tag changes what you are running with no version changing.`
|
|
2149
|
+
};
|
|
2150
|
+
return { tag: cliVersion };
|
|
2151
|
+
}
|
|
2016
2152
|
async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
2017
2153
|
const envExamplePath = path.join(targetDirectory, ".env.example");
|
|
2018
2154
|
const envPath = path.join(targetDirectory, ".env");
|
|
@@ -2031,8 +2167,10 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
2031
2167
|
envContent = envContent.replace(/^#\s*REBASE_SERVICE_KEY=.*$/m, `REBASE_SERVICE_KEY=${serviceKey}`);
|
|
2032
2168
|
const composeApiPort = /^PORT=(\d+)/m.exec(envContent)?.[1] ?? "3001";
|
|
2033
2169
|
envContent = envContent.replace(/^#\s*CORS_ORIGINS=.*$/m, `CORS_ORIGINS=http://localhost:${composeApiPort}`);
|
|
2034
|
-
|
|
2035
|
-
|
|
2170
|
+
envContent = envContent.replace(/^#?\s*VITE_API_URL=.*$/m, "VITE_API_URL=");
|
|
2171
|
+
const { tag: runtimeVersion, note } = resolveRuntimeImageTag(readCliVersion());
|
|
2172
|
+
const pinned = `${note ? `${note}\n` : ""}REBASE_VERSION=${runtimeVersion}`;
|
|
2173
|
+
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
2174
|
if (databaseUrl) {
|
|
2037
2175
|
if (/[\r\n]/.test(databaseUrl)) throw new Error("Invalid DATABASE_URL: multiline values are not allowed.");
|
|
2038
2176
|
const { pinSearchPath } = await import("@rebasepro/server-postgres");
|
|
@@ -2040,7 +2178,7 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
2040
2178
|
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${pinnedUrl}\nDATABASE_PASSWORD=${dbPassword}`);
|
|
2041
2179
|
} else {
|
|
2042
2180
|
const dbPort = await findAvailablePort(5432);
|
|
2043
|
-
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://
|
|
2181
|
+
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://rebase_app:${dbPassword}@127.0.0.1:${dbPort}/rebase?options=-c%20search_path=public&sslmode=disable\nDATABASE_PASSWORD=${dbPassword}`);
|
|
2044
2182
|
const dockerComposePath = path.join(targetDirectory, "docker-compose.yml");
|
|
2045
2183
|
if (fs.existsSync(dockerComposePath)) {
|
|
2046
2184
|
let dockerComposeContent = fs.readFileSync(dockerComposePath, "utf-8");
|
|
@@ -2367,7 +2505,7 @@ async function schemaCommand(subcommand, rawArgs) {
|
|
|
2367
2505
|
return;
|
|
2368
2506
|
}
|
|
2369
2507
|
const projectRoot = requireProjectRoot();
|
|
2370
|
-
recordEvent("cli.
|
|
2508
|
+
recordEvent("cli.schema", { subcommand: subcommand ?? "none" }, { projectRoot });
|
|
2371
2509
|
const backendDir = requireBackendDir(projectRoot);
|
|
2372
2510
|
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
2373
2511
|
if (!activePlugin) {
|
|
@@ -2436,7 +2574,7 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
2436
2574
|
return;
|
|
2437
2575
|
}
|
|
2438
2576
|
const projectRoot = requireProjectRoot();
|
|
2439
|
-
recordEvent("cli.
|
|
2577
|
+
recordEvent("cli.db", { subcommand: subcommand ?? "none" }, { projectRoot });
|
|
2440
2578
|
const backendDir = requireBackendDir(projectRoot);
|
|
2441
2579
|
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
2442
2580
|
if (!activePlugin) {
|
|
@@ -3074,6 +3212,106 @@ function resolveBackendPaths(app, projectRoot) {
|
|
|
3074
3212
|
};
|
|
3075
3213
|
}
|
|
3076
3214
|
//#endregion
|
|
3215
|
+
//#region src/utils/collection-drift.ts
|
|
3216
|
+
/**
|
|
3217
|
+
* Which edits under `config/collections` can put the *SQL* schema out of date.
|
|
3218
|
+
*
|
|
3219
|
+
* `rebase dev` watches that directory and, on any change, either tells the
|
|
3220
|
+
* developer to run `rebase schema generate` / `rebase db push` or runs them.
|
|
3221
|
+
* The watcher is recursive and knows nothing about what it is watching, so it
|
|
3222
|
+
* said that about every file under the directory — including a
|
|
3223
|
+
* `collections/firestore/exercises.ts`, whose documents live in Firestore and
|
|
3224
|
+
* for which there is no Drizzle schema to regenerate and no database to push
|
|
3225
|
+
* to. Advice that is wrong on every edit is advice a developer learns to
|
|
3226
|
+
* ignore, which is worse than none: the same box is the only warning for the
|
|
3227
|
+
* Postgres collection next to it, where it is real.
|
|
3228
|
+
*
|
|
3229
|
+
* Two independent reasons a change cannot affect the SQL schema, both checked
|
|
3230
|
+
* here:
|
|
3231
|
+
*
|
|
3232
|
+
* 1. **The loader would never read the file.** `loadCollectionsFromDirectory`
|
|
3233
|
+
* reads the top level of the collections directory only — no recursion, no
|
|
3234
|
+
* `index`, no tests, no declarations. A file it does not read cannot change
|
|
3235
|
+
* what it returns, and the watcher must not claim otherwise.
|
|
3236
|
+
* 2. **Every collection in it is served by another engine.** A Firestore or
|
|
3237
|
+
* MongoDB collection has no table, no migration and no policies.
|
|
3238
|
+
*
|
|
3239
|
+
* The engine check reads the source text rather than importing the module: the
|
|
3240
|
+
* CLI runs as plain Node and cannot evaluate a project's TypeScript, and a
|
|
3241
|
+
* watcher must answer in the time between two keystrokes. Anything it cannot
|
|
3242
|
+
* read confidently counts as SQL-affecting — a spurious warning is a nuisance,
|
|
3243
|
+
* a suppressed one hides real drift.
|
|
3244
|
+
*/
|
|
3245
|
+
/**
|
|
3246
|
+
* Would `loadCollectionsFromDirectory` load this file?
|
|
3247
|
+
*
|
|
3248
|
+
* Mirrors that loader's own `isCollectionFile` plus its flat (non-recursive)
|
|
3249
|
+
* scan. `relativePath` is relative to the collections directory, as `fs.watch`
|
|
3250
|
+
* reports it.
|
|
3251
|
+
*/
|
|
3252
|
+
function isLoadedCollectionFile(relativePath) {
|
|
3253
|
+
const normalized = relativePath.split(path.sep).join("/");
|
|
3254
|
+
if (normalized.includes("/")) return false;
|
|
3255
|
+
const file = normalized;
|
|
3256
|
+
if (!file.endsWith(".ts") && !file.endsWith(".js")) return false;
|
|
3257
|
+
if (file.startsWith(".")) return false;
|
|
3258
|
+
if (file.includes(".test.")) return false;
|
|
3259
|
+
if (file.endsWith(".d.ts")) return false;
|
|
3260
|
+
if (file === "index.ts" || file === "index.js") return false;
|
|
3261
|
+
return true;
|
|
3262
|
+
}
|
|
3263
|
+
var ENGINE_LITERAL = /\bengine\s*:\s*["'`]([^"'`]+)["'`]/g;
|
|
3264
|
+
var DATA_SOURCE_LITERAL = /\bdataSource\s*:\s*["'`]([^"'`]+)["'`]/g;
|
|
3265
|
+
/**
|
|
3266
|
+
* Drop comments, so a `// engine: "firestore"` in a docblock cannot silence a
|
|
3267
|
+
* warning about the Postgres collection the file actually declares.
|
|
3268
|
+
*
|
|
3269
|
+
* Deliberately naive — it does not understand strings, so a `//` inside one
|
|
3270
|
+
* (a URL in a default value) eats the rest of that line. That only ever removes
|
|
3271
|
+
* text, and removing an engine literal makes this file *more* likely to warn,
|
|
3272
|
+
* which is the side to be wrong on.
|
|
3273
|
+
*/
|
|
3274
|
+
function stripComments(source) {
|
|
3275
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
|
|
3276
|
+
}
|
|
3277
|
+
function literals(source, pattern) {
|
|
3278
|
+
const found = [];
|
|
3279
|
+
for (const match of source.matchAll(pattern)) found.push(match[1]);
|
|
3280
|
+
return found;
|
|
3281
|
+
}
|
|
3282
|
+
/**
|
|
3283
|
+
* Does this collection source declare anything a SQL toolchain would own?
|
|
3284
|
+
*
|
|
3285
|
+
* The fallback order matches `resolveDataSource`: an explicit `engine` wins,
|
|
3286
|
+
* and a collection that only names a `dataSource` is resolved as if the key
|
|
3287
|
+
* were the engine — which is what that function does when no registry is
|
|
3288
|
+
* available, and the CLI has none. An engine nobody recognises counts as
|
|
3289
|
+
* relational, for the reason `isRelationalCollection` gives.
|
|
3290
|
+
*
|
|
3291
|
+
* A file declaring several collections is SQL-affecting if *any* of them is.
|
|
3292
|
+
*/
|
|
3293
|
+
function declaresRelationalCollection(rawSource) {
|
|
3294
|
+
const source = stripComments(rawSource);
|
|
3295
|
+
const engines = literals(source, ENGINE_LITERAL);
|
|
3296
|
+
const declared = engines.length > 0 ? engines : literals(source, DATA_SOURCE_LITERAL).filter((key) => key !== DEFAULT_DATA_SOURCE_KEY);
|
|
3297
|
+
if (declared.length === 0) return true;
|
|
3298
|
+
return declared.some((engine) => getDataSourceCapabilities(engine).supportsRelations);
|
|
3299
|
+
}
|
|
3300
|
+
/**
|
|
3301
|
+
* Can this edit have changed the generated SQL schema?
|
|
3302
|
+
*
|
|
3303
|
+
* Answers `true` when it cannot tell — an unreadable file is a reason to warn,
|
|
3304
|
+
* not a reason to go quiet.
|
|
3305
|
+
*/
|
|
3306
|
+
function affectsSqlSchema(collectionsDir, relativePath) {
|
|
3307
|
+
if (!isLoadedCollectionFile(relativePath)) return false;
|
|
3308
|
+
try {
|
|
3309
|
+
return declaresRelationalCollection(fs.readFileSync(path.join(collectionsDir, relativePath), "utf8"));
|
|
3310
|
+
} catch {
|
|
3311
|
+
return true;
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
//#endregion
|
|
3077
3315
|
//#region src/commands/dev.ts
|
|
3078
3316
|
/**
|
|
3079
3317
|
* CLI command: rebase dev
|
|
@@ -3167,14 +3405,33 @@ function getProjectPort(projectRoot) {
|
|
|
3167
3405
|
* 3. Previously used port from .rebase-dev-port (port affinity across restarts)
|
|
3168
3406
|
* 4. Deterministic hash from project path (unique per project)
|
|
3169
3407
|
*/
|
|
3408
|
+
/**
|
|
3409
|
+
* A TCP port, or `undefined` for anything that is not one.
|
|
3410
|
+
*
|
|
3411
|
+
* One predicate for both sources below. The port file was already checked for
|
|
3412
|
+
* range, and `PORT` — the source a human or a platform actually sets — was not,
|
|
3413
|
+
* so `PORT=oops` reached `parseInt` and was returned as `NaN`: the dev server
|
|
3414
|
+
* then bound to whatever the OS handed out and the CLI printed a URL for a port
|
|
3415
|
+
* nothing was listening on.
|
|
3416
|
+
*/
|
|
3417
|
+
function parsePort(raw) {
|
|
3418
|
+
if (raw === void 0) return void 0;
|
|
3419
|
+
const port = Number(raw.trim());
|
|
3420
|
+
if (!Number.isInteger(port) || port <= 0 || port >= 65536) return void 0;
|
|
3421
|
+
return port;
|
|
3422
|
+
}
|
|
3170
3423
|
function resolveStartPort(projectRoot, explicitPort) {
|
|
3171
3424
|
if (explicitPort) return explicitPort;
|
|
3172
|
-
if (process.env.PORT)
|
|
3425
|
+
if (process.env.PORT) {
|
|
3426
|
+
const fromEnv = parsePort(process.env.PORT);
|
|
3427
|
+
if (fromEnv !== void 0) return fromEnv;
|
|
3428
|
+
console.warn(chalk.yellow(` ⚠ Ignoring PORT="${process.env.PORT}" — not a port between 1 and 65535.`));
|
|
3429
|
+
}
|
|
3173
3430
|
try {
|
|
3174
3431
|
const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
|
|
3175
3432
|
if (fs.existsSync(portFile)) {
|
|
3176
|
-
const saved =
|
|
3177
|
-
if (saved
|
|
3433
|
+
const saved = parsePort(fs.readFileSync(portFile, "utf-8"));
|
|
3434
|
+
if (saved !== void 0) return saved;
|
|
3178
3435
|
}
|
|
3179
3436
|
} catch {}
|
|
3180
3437
|
return getProjectPort(projectRoot);
|
|
@@ -3188,7 +3445,7 @@ async function devCommand(rawArgs) {
|
|
|
3188
3445
|
"--help": Boolean,
|
|
3189
3446
|
"-b": "--backend-only",
|
|
3190
3447
|
"-f": "--frontend-only",
|
|
3191
|
-
"-
|
|
3448
|
+
"-P": "--port",
|
|
3192
3449
|
"-g": "--generate",
|
|
3193
3450
|
"-h": "--help"
|
|
3194
3451
|
}, {
|
|
@@ -3345,6 +3602,20 @@ async function devCommand(rawArgs) {
|
|
|
3345
3602
|
} catch {}
|
|
3346
3603
|
/** Whether the frontend has been launched (we only launch it once). */
|
|
3347
3604
|
let frontendLaunched = false;
|
|
3605
|
+
try {
|
|
3606
|
+
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
3607
|
+
const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
|
|
3608
|
+
if (pluginCli) await execa(tsxBin, [
|
|
3609
|
+
pluginCli,
|
|
3610
|
+
"schema",
|
|
3611
|
+
"stale",
|
|
3612
|
+
"--fix"
|
|
3613
|
+
], {
|
|
3614
|
+
cwd: backendDir,
|
|
3615
|
+
stdio: "inherit",
|
|
3616
|
+
env
|
|
3617
|
+
});
|
|
3618
|
+
} catch {}
|
|
3348
3619
|
if (shouldGenerate) {
|
|
3349
3620
|
console.log(chalk.gray(" → Ensuring schema and SDK are generated on start..."));
|
|
3350
3621
|
try {
|
|
@@ -3372,14 +3643,18 @@ async function devCommand(rawArgs) {
|
|
|
3372
3643
|
const collectionsDir = path.join(projectRoot, "config", "collections");
|
|
3373
3644
|
if (fs.existsSync(collectionsDir)) {
|
|
3374
3645
|
let watchDebounce = null;
|
|
3646
|
+
let sqlSchemaAffected = false;
|
|
3375
3647
|
fs.watch(collectionsDir, { recursive: true }, (eventType, filename) => {
|
|
3376
3648
|
if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
|
|
3649
|
+
sqlSchemaAffected = sqlSchemaAffected || affectsSqlSchema(collectionsDir, filename);
|
|
3377
3650
|
if (watchDebounce) clearTimeout(watchDebounce);
|
|
3378
3651
|
watchDebounce = setTimeout(async () => {
|
|
3379
|
-
|
|
3652
|
+
const regenerateSchema = sqlSchemaAffected;
|
|
3653
|
+
sqlSchemaAffected = false;
|
|
3654
|
+
console.log(chalk.yellow(`\n 🔄 Collection change detected (${filename}). Regenerating ${regenerateSchema ? "schema & SDK" : "SDK"}...`));
|
|
3380
3655
|
try {
|
|
3381
3656
|
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
3382
|
-
const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
|
|
3657
|
+
const pluginCli = regenerateSchema && activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
|
|
3383
3658
|
if (pluginCli) await execa(tsxBin, [
|
|
3384
3659
|
pluginCli,
|
|
3385
3660
|
"schema",
|
|
@@ -3395,7 +3670,7 @@ async function devCommand(rawArgs) {
|
|
|
3395
3670
|
stdio: "inherit",
|
|
3396
3671
|
env
|
|
3397
3672
|
});
|
|
3398
|
-
console.log(chalk.green(
|
|
3673
|
+
console.log(chalk.green(` ✓ ${regenerateSchema ? "Schema & SDK" : "SDK"} regenerated successfully. Hono will reload.`));
|
|
3399
3674
|
} catch (err) {
|
|
3400
3675
|
console.error(chalk.red(` ✗ Failed to regenerate schema/SDK: ${err instanceof Error ? err.message : err}`));
|
|
3401
3676
|
}
|
|
@@ -3420,12 +3695,14 @@ async function devCommand(rawArgs) {
|
|
|
3420
3695
|
let driftDebounce = null;
|
|
3421
3696
|
fs.watch(collectionsDir, { recursive: true }, (_eventType, filename) => {
|
|
3422
3697
|
if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
|
|
3698
|
+
if (!affectsSqlSchema(collectionsDir, filename)) return;
|
|
3423
3699
|
if (driftDebounce) clearTimeout(driftDebounce);
|
|
3424
3700
|
driftDebounce = setTimeout(() => {
|
|
3701
|
+
const shown = filename.length > 31 ? `…${filename.slice(-30)}` : filename.padEnd(31);
|
|
3425
3702
|
console.log([
|
|
3426
3703
|
"",
|
|
3427
3704
|
chalk.yellow(" ┌──────────────────────────────────────────────────────────────┐"),
|
|
3428
|
-
chalk.yellow(" │ ⚠️ Collection file changed: ") + chalk.white(
|
|
3705
|
+
chalk.yellow(" │ ⚠️ Collection file changed: ") + chalk.white(shown) + chalk.yellow("│"),
|
|
3429
3706
|
chalk.yellow(" │ │"),
|
|
3430
3707
|
chalk.yellow(" │ Your schema may be out of sync. Run: │"),
|
|
3431
3708
|
chalk.yellow(" │ ") + chalk.cyan("rebase schema generate") + chalk.yellow(" regenerate Drizzle schema │"),
|
|
@@ -4651,6 +4928,61 @@ function assertBuiltForPath(indexHtml, basePath, appName) {
|
|
|
4651
4928
|
build config — see docs/apps-and-runtimes.md §4.2.`);
|
|
4652
4929
|
}
|
|
4653
4930
|
/**
|
|
4931
|
+
* The environment every static app is built with, wherever that build is driven from.
|
|
4932
|
+
*
|
|
4933
|
+
* Shared because there are two drivers — `foldFrontendIntoBundle` here, and
|
|
4934
|
+
* `buildAssetApp` in `build.ts` for a standalone `type: "static"` app — and they
|
|
4935
|
+
* had already drifted: the path variables were duplicated into both, so a fix
|
|
4936
|
+
* applied to one shipped a bundle built the old way from the other. One function
|
|
4937
|
+
* makes that impossible rather than merely unlikely.
|
|
4938
|
+
*
|
|
4939
|
+
* ## REBASE_APP_*
|
|
4940
|
+
*
|
|
4941
|
+
* The declared path is a build-time input, not only a serving concern: Vite
|
|
4942
|
+
* reads `base` from REBASE_APP_BASE, and the trailing slash is that field's
|
|
4943
|
+
* convention. See `assertBuiltForPath`.
|
|
4944
|
+
*
|
|
4945
|
+
* ## NODE_ENV
|
|
4946
|
+
*
|
|
4947
|
+
* A built app is a production artifact by construction, so build it as one. Not
|
|
4948
|
+
* a formality: the scaffold's `.env` carries `NODE_ENV=development` for the dev
|
|
4949
|
+
* backend, and Vite's `loadEnv` promotes a `NODE_ENV` found in an env file into
|
|
4950
|
+
* the build unless the environment already sets one. So `rebase build` and
|
|
4951
|
+
* `rebase cloud deploy` shipped a *development* bundle — `import.meta.env.DEV
|
|
4952
|
+
* === true`, development React, dev-only branches live — from commands whose
|
|
4953
|
+
* whole purpose is to produce something deployable. Setting it here is what
|
|
4954
|
+
* closes it: Vite consults the env file's NODE_ENV only when `process.env`
|
|
4955
|
+
* has none.
|
|
4956
|
+
*
|
|
4957
|
+
* ## VITE_API_URL
|
|
4958
|
+
*
|
|
4959
|
+
* An app served by the backend it talks to has its API on its own origin by
|
|
4960
|
+
* construction, so a baked-in absolute URL can only be wrong. It was: that same
|
|
4961
|
+
* `.env` carries `VITE_API_URL=http://localhost:3001` and
|
|
4962
|
+
* `frontend/vite.config.ts` reads the project root via `envDir: ".."`, so a
|
|
4963
|
+
* stock deploy shipped a site whose every request went to whoever ran the build
|
|
4964
|
+
* — passing every server-side health check on the way out. Blanking it here
|
|
4965
|
+
* fixes the bundle even for a project whose `.env` predates the `init` fix, or
|
|
4966
|
+
* was written by hand. Empty is the right value rather than a missing one: the
|
|
4967
|
+
* client falls back to `window.location.origin`, which keeps working when a
|
|
4968
|
+
* custom domain is added.
|
|
4969
|
+
*
|
|
4970
|
+
* Vite prioritises `process.env.VITE_*` over `.env` files, so an explicit
|
|
4971
|
+
* `VITE_API_URL=https://api.example.com rebase cloud deploy` still wins — the
|
|
4972
|
+
* cross-origin escape hatch stays open, it just has to be deliberate. Nothing on
|
|
4973
|
+
* this path loads the project `.env` into `process.env`, so a value inherited
|
|
4974
|
+
* here really was set by the caller.
|
|
4975
|
+
*/
|
|
4976
|
+
function staticBuildEnv(appPath, appName) {
|
|
4977
|
+
return {
|
|
4978
|
+
REBASE_APP_PATH: appPath,
|
|
4979
|
+
REBASE_APP_BASE: appPath === "/" ? "/" : `${appPath}/`,
|
|
4980
|
+
REBASE_APP_NAME: appName,
|
|
4981
|
+
NODE_ENV: "production",
|
|
4982
|
+
VITE_API_URL: process.env.VITE_API_URL ?? ""
|
|
4983
|
+
};
|
|
4984
|
+
}
|
|
4985
|
+
/**
|
|
4654
4986
|
* Build the project's static apps and fold them into the backend bundle.
|
|
4655
4987
|
*
|
|
4656
4988
|
* Throws rather than exiting, so the caller decides whether a missing frontend
|
|
@@ -4669,11 +5001,7 @@ async function foldFrontendIntoBundle(options) {
|
|
|
4669
5001
|
cwd: projectRoot,
|
|
4670
5002
|
stdio: "inherit",
|
|
4671
5003
|
shell: true,
|
|
4672
|
-
env:
|
|
4673
|
-
REBASE_APP_PATH: app.path,
|
|
4674
|
-
REBASE_APP_BASE: app.path === "/" ? "/" : `${app.path}/`,
|
|
4675
|
-
REBASE_APP_NAME: app.name
|
|
4676
|
-
}
|
|
5004
|
+
env: staticBuildEnv(app.path, app.name)
|
|
4677
5005
|
});
|
|
4678
5006
|
const assetsDir = path.join(projectRoot, app.output);
|
|
4679
5007
|
if (!fs.existsSync(assetsDir)) throw new Error(`"${app.name}" declared output "${app.output}" does not exist after building — the bundle would ship without a frontend.`);
|
|
@@ -4732,7 +5060,8 @@ ${chalk.bold("Examples")}
|
|
|
4732
5060
|
}
|
|
4733
5061
|
async function buildCommand(rawArgs = []) {
|
|
4734
5062
|
const args = arg({
|
|
4735
|
-
"--
|
|
5063
|
+
"--output": String,
|
|
5064
|
+
"--out": "--output",
|
|
4736
5065
|
"--skip-type-check": Boolean,
|
|
4737
5066
|
"--skip-schema": Boolean,
|
|
4738
5067
|
"--no-static": Boolean,
|
|
@@ -4800,7 +5129,7 @@ async function buildCommand(rawArgs = []) {
|
|
|
4800
5129
|
projectRoot,
|
|
4801
5130
|
appName: name,
|
|
4802
5131
|
app,
|
|
4803
|
-
outDir: args["--
|
|
5132
|
+
outDir: args["--output"],
|
|
4804
5133
|
runtimeRange: manifest.rebase,
|
|
4805
5134
|
storage: manifest.storage,
|
|
4806
5135
|
skipTypeCheck: args["--skip-type-check"],
|
|
@@ -4837,7 +5166,7 @@ async function buildCommand(rawArgs = []) {
|
|
|
4837
5166
|
});
|
|
4838
5167
|
for (const outcome of folded ?? []) console.log(chalk.green(` ✓ ${outcome.appName} folded in`) + chalk.dim(` (${outcome.fileCount} file(s) → served at ${outcome.path})`));
|
|
4839
5168
|
}
|
|
4840
|
-
} else if (app.type === "static") await buildAssetApp(projectRoot, name, app, manifest.rebase, args["--
|
|
5169
|
+
} else if (app.type === "static") await buildAssetApp(projectRoot, name, app, manifest.rebase, args["--output"]);
|
|
4841
5170
|
console.log("");
|
|
4842
5171
|
}
|
|
4843
5172
|
console.log(chalk.green("✓ Build complete."));
|
|
@@ -4862,11 +5191,7 @@ async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride)
|
|
|
4862
5191
|
cwd: projectRoot,
|
|
4863
5192
|
stdio: "inherit",
|
|
4864
5193
|
shell: true,
|
|
4865
|
-
env:
|
|
4866
|
-
REBASE_APP_PATH: basePath,
|
|
4867
|
-
REBASE_APP_BASE: basePath === "/" ? "/" : `${basePath}/`,
|
|
4868
|
-
REBASE_APP_NAME: name
|
|
4869
|
-
}
|
|
5194
|
+
env: staticBuildEnv(basePath, name)
|
|
4870
5195
|
});
|
|
4871
5196
|
} catch {
|
|
4872
5197
|
console.error(chalk.red(` ✗ build command failed for "${name}"`));
|
|
@@ -5286,6 +5611,48 @@ async function startWorkspaceBackend(projectRoot, env) {
|
|
|
5286
5611
|
* Subcommands:
|
|
5287
5612
|
* reset-password — Reset a user's password
|
|
5288
5613
|
*/
|
|
5614
|
+
/**
|
|
5615
|
+
* Pick the user with exactly this email out of a search response.
|
|
5616
|
+
*
|
|
5617
|
+
* `/api/admin/users?search=` is an `ILIKE '%…%'` over email **or display
|
|
5618
|
+
* name**, ordered by role count descending. This used to take row `[0]` and
|
|
5619
|
+
* reset it, then print the email it had been *given* as confirmation — so two
|
|
5620
|
+
* ordinary situations ended in a successful-looking reset of somebody else's
|
|
5621
|
+
* account:
|
|
5622
|
+
*
|
|
5623
|
+
* - a substring collision: `bob@example.com` also matches
|
|
5624
|
+
* `robert.bob@example.com`;
|
|
5625
|
+
* - a display name, which is user-controlled and accepted up to 255
|
|
5626
|
+
* characters with no constraint on its content, containing an address
|
|
5627
|
+
* belonging to someone else.
|
|
5628
|
+
*
|
|
5629
|
+
* The ordering makes it worse rather than better — `array_length(roles) DESC
|
|
5630
|
+
* NULLS LAST` puts the most privileged match first, so the account most likely
|
|
5631
|
+
* to be reset by mistake is an admin's.
|
|
5632
|
+
*
|
|
5633
|
+
* Returns `undefined` when nothing matched exactly, which the caller reports
|
|
5634
|
+
* rather than falling through to a guess. The direct-database fallback below
|
|
5635
|
+
* has always matched with `eq(usersTable.email, email)`; this is the same
|
|
5636
|
+
* definition, so the command no longer resets different accounts depending on
|
|
5637
|
+
* whether the backend happened to be running.
|
|
5638
|
+
*/
|
|
5639
|
+
function selectUserForEmail(payload, email) {
|
|
5640
|
+
const wanted = email.trim().toLowerCase();
|
|
5641
|
+
if (!wanted) return void 0;
|
|
5642
|
+
const rows = Array.isArray(payload) ? payload : payload && typeof payload === "object" && Array.isArray(payload.users) ? payload.users : [];
|
|
5643
|
+
for (const row of rows) {
|
|
5644
|
+
if (!row || typeof row !== "object") continue;
|
|
5645
|
+
const record = row;
|
|
5646
|
+
const rowEmail = typeof record.email === "string" ? record.email.trim().toLowerCase() : void 0;
|
|
5647
|
+
if (!rowEmail || rowEmail !== wanted) continue;
|
|
5648
|
+
const id = typeof record.id === "string" ? record.id : typeof record.uid === "string" ? record.uid : void 0;
|
|
5649
|
+
if (!id) continue;
|
|
5650
|
+
return {
|
|
5651
|
+
id,
|
|
5652
|
+
email: record.email
|
|
5653
|
+
};
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5289
5656
|
async function authCommand(subcommand, rawArgs) {
|
|
5290
5657
|
if (!subcommand || subcommand === "--help") {
|
|
5291
5658
|
printAuthHelp();
|
|
@@ -5306,8 +5673,7 @@ async function resetPassword(rawArgs) {
|
|
|
5306
5673
|
const args = arg({
|
|
5307
5674
|
"--email": String,
|
|
5308
5675
|
"--password": String,
|
|
5309
|
-
"-e": "--email"
|
|
5310
|
-
"-p": "--password"
|
|
5676
|
+
"-e": "--email"
|
|
5311
5677
|
}, {
|
|
5312
5678
|
argv: rawArgs.slice(4),
|
|
5313
5679
|
permissive: true
|
|
@@ -5322,12 +5688,8 @@ async function resetPassword(rawArgs) {
|
|
|
5322
5688
|
process.exit(1);
|
|
5323
5689
|
}
|
|
5324
5690
|
const projectRoot = requireProjectRoot();
|
|
5325
|
-
let envServiceKey;
|
|
5326
5691
|
const envFile = findEnvFile(projectRoot);
|
|
5327
|
-
|
|
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 {}
|
|
5692
|
+
const envServiceKey = readEnvFile(projectRoot).REBASE_SERVICE_KEY;
|
|
5331
5693
|
let baseUrl = process.env.REBASE_BASE_URL;
|
|
5332
5694
|
let serviceKey = process.env.REBASE_SERVICE_KEY || envServiceKey;
|
|
5333
5695
|
const statePath = path.join(projectRoot, ".rebase", "state.json");
|
|
@@ -5347,7 +5709,7 @@ async function resetPassword(rawArgs) {
|
|
|
5347
5709
|
try {
|
|
5348
5710
|
const finalPass = newPassword || "NewPassword123!";
|
|
5349
5711
|
const cleanBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
5350
|
-
const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=
|
|
5712
|
+
const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=50`;
|
|
5351
5713
|
const searchRes = await fetch(searchUrl, { headers: {
|
|
5352
5714
|
"Authorization": `Bearer ${serviceKey}`,
|
|
5353
5715
|
"Accept": "application/json"
|
|
@@ -5355,18 +5717,9 @@ async function resetPassword(rawArgs) {
|
|
|
5355
5717
|
if (!searchRes.ok) throw new Error(`Failed to list users: ${searchRes.statusText}`);
|
|
5356
5718
|
const searchData = await searchRes.json();
|
|
5357
5719
|
if (!searchData || typeof searchData !== "object") throw new Error("Invalid response format from user search API.");
|
|
5358
|
-
|
|
5359
|
-
if (
|
|
5360
|
-
|
|
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`;
|
|
5720
|
+
const matched = selectUserForEmail(searchData, email);
|
|
5721
|
+
if (!matched) throw new Error(`No user has the email ${email}.`);
|
|
5722
|
+
const resetUrl = `${cleanBaseUrl}/api/admin/users/${matched.id}/reset-password`;
|
|
5370
5723
|
const resetRes = await fetch(resetUrl, {
|
|
5371
5724
|
method: "POST",
|
|
5372
5725
|
headers: {
|
|
@@ -5383,7 +5736,7 @@ async function resetPassword(rawArgs) {
|
|
|
5383
5736
|
console.log("API reset successful.");
|
|
5384
5737
|
console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (via API)"));
|
|
5385
5738
|
console.log("");
|
|
5386
|
-
console.log(` ${chalk.gray("Email:")} ${email}`);
|
|
5739
|
+
console.log(` ${chalk.gray("Email:")} ${matched.email}`);
|
|
5387
5740
|
console.log(` ${chalk.gray("Password:")} ${finalPass}`);
|
|
5388
5741
|
console.log("");
|
|
5389
5742
|
return;
|
|
@@ -5451,10 +5804,12 @@ async function resetPassword() {
|
|
|
5451
5804
|
if (result.length > 0) {
|
|
5452
5805
|
console.log("✅ Password reset for: " + result[0].email);
|
|
5453
5806
|
${!newPassword ? "console.log(\" New password: \" + newPassword);" : ""}
|
|
5454
|
-
|
|
5455
|
-
console.log("✗ User not found: " + email);
|
|
5807
|
+
process.exit(0);
|
|
5456
5808
|
}
|
|
5457
|
-
|
|
5809
|
+
// Nothing was updated, so nothing was reset. Exiting 0 here reported
|
|
5810
|
+
// success for a no-op, which is what a script would have believed.
|
|
5811
|
+
console.error("✗ User not found: " + email);
|
|
5812
|
+
process.exit(1);
|
|
5458
5813
|
}
|
|
5459
5814
|
|
|
5460
5815
|
resetPassword().catch(console.error);
|
|
@@ -5472,11 +5827,20 @@ resetPassword().catch(console.error);
|
|
|
5472
5827
|
stdio: "inherit",
|
|
5473
5828
|
env
|
|
5474
5829
|
});
|
|
5830
|
+
const cleanup = () => {
|
|
5831
|
+
try {
|
|
5832
|
+
fs.unlinkSync(tmpScriptPath);
|
|
5833
|
+
} catch {}
|
|
5834
|
+
};
|
|
5475
5835
|
return new Promise((resolve) => {
|
|
5836
|
+
child.on("error", (err) => {
|
|
5837
|
+
cleanup();
|
|
5838
|
+
console.error(chalk.red("✗ Could not run the reset script."));
|
|
5839
|
+
console.error(chalk.gray(` ${err.message}`));
|
|
5840
|
+
process.exit(1);
|
|
5841
|
+
});
|
|
5476
5842
|
child.on("close", (code) => {
|
|
5477
|
-
|
|
5478
|
-
fs.unlinkSync(tmpScriptPath);
|
|
5479
|
-
} catch {}
|
|
5843
|
+
cleanup();
|
|
5480
5844
|
if (code !== 0) process.exit(code ?? 1);
|
|
5481
5845
|
resolve();
|
|
5482
5846
|
});
|
|
@@ -5514,7 +5878,34 @@ ${chalk.green.bold("Examples")}
|
|
|
5514
5878
|
* Detects three-way schema drift between collection definitions,
|
|
5515
5879
|
* the generated Drizzle schema, and the live PostgreSQL database.
|
|
5516
5880
|
*/
|
|
5881
|
+
/**
|
|
5882
|
+
* `--help` is answered before the project guard, not after.
|
|
5883
|
+
*
|
|
5884
|
+
* `doctor` declared no `--help` at all, so the flag fell through to the command
|
|
5885
|
+
* body and hit `requireProjectRoot()` — and `rebase doctor --help` outside a
|
|
5886
|
+
* project answered "✗ Could not find a Rebase project root." Asking a command
|
|
5887
|
+
* what it does is the one question that cannot require being somewhere
|
|
5888
|
+
* particular to ask.
|
|
5889
|
+
*/
|
|
5890
|
+
function printDoctorHelp() {
|
|
5891
|
+
console.log(`
|
|
5892
|
+
${chalk.bold("rebase doctor")} — Detect drift between collections, schema and database
|
|
5893
|
+
|
|
5894
|
+
${chalk.green.bold("Usage")}
|
|
5895
|
+
rebase doctor
|
|
5896
|
+
|
|
5897
|
+
Compares the collections you declare, the generated Drizzle schema, and the
|
|
5898
|
+
tables that actually exist, then reports what disagrees and how to reconcile it.
|
|
5899
|
+
|
|
5900
|
+
Run from inside a Rebase project — it reads the project's collections and
|
|
5901
|
+
connects to its database.
|
|
5902
|
+
`);
|
|
5903
|
+
}
|
|
5517
5904
|
async function doctorCommand(rawArgs) {
|
|
5905
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
5906
|
+
printDoctorHelp();
|
|
5907
|
+
return;
|
|
5908
|
+
}
|
|
5518
5909
|
const projectRoot = requireProjectRoot();
|
|
5519
5910
|
const backendDir = requireBackendDir(projectRoot);
|
|
5520
5911
|
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
@@ -5663,7 +6054,8 @@ async function skillsCommand(subcommand, rawArgs) {
|
|
|
5663
6054
|
}
|
|
5664
6055
|
/**
|
|
5665
6056
|
* 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
|
|
6057
|
+
* (also accepts a comma-separated list, and `all`). Returns null when none were
|
|
6058
|
+
* given.
|
|
5667
6059
|
*/
|
|
5668
6060
|
function parseAgentFlags(rawArgs) {
|
|
5669
6061
|
const requested = [];
|
|
@@ -5673,6 +6065,7 @@ function parseAgentFlags(rawArgs) {
|
|
|
5673
6065
|
if (value && !value.startsWith("-")) requested.push(...value.split(",").map((v) => v.trim()).filter(Boolean));
|
|
5674
6066
|
}
|
|
5675
6067
|
if (requested.length === 0) return null;
|
|
6068
|
+
if (requested.includes("all")) return Object.keys(AGENTS);
|
|
5676
6069
|
const valid = Object.keys(AGENTS);
|
|
5677
6070
|
const unknown = requested.filter((a) => !valid.includes(a));
|
|
5678
6071
|
if (unknown.length > 0) {
|
|
@@ -5700,6 +6093,7 @@ async function skillsInstall(rawArgs = []) {
|
|
|
5700
6093
|
if (!process.stdin.isTTY) {
|
|
5701
6094
|
console.error(chalk.red("Cannot prompt: this is a non-interactive terminal (no TTY)."));
|
|
5702
6095
|
console.error(chalk.yellow(` Name the agents explicitly, e.g. rebase skills install --agent ${Object.keys(AGENTS)[0]}`));
|
|
6096
|
+
console.error(chalk.yellow(" Or install for every supported agent: rebase skills install --agent all"));
|
|
5703
6097
|
console.error(chalk.gray(` Available: ${Object.keys(AGENTS).join(", ")}`));
|
|
5704
6098
|
process.exit(1);
|
|
5705
6099
|
}
|
|
@@ -5747,13 +6141,16 @@ ${chalk.green.bold("Subcommands")}
|
|
|
5747
6141
|
|
|
5748
6142
|
${chalk.green.bold("Options")}
|
|
5749
6143
|
${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
|
-
|
|
6144
|
+
Repeat the flag or pass a comma-separated list, or ${chalk.bold("all")}.
|
|
6145
|
+
Required without a TTY: a scaffolded project carries a marker
|
|
6146
|
+
file for every agent, so detection cannot pick one for you.
|
|
6147
|
+
Available: ${Object.keys(AGENTS).join(", ")}, all
|
|
5752
6148
|
|
|
5753
6149
|
${chalk.green.bold("Examples")}
|
|
5754
6150
|
${chalk.cyan("rebase skills install")}
|
|
5755
6151
|
${chalk.cyan("rebase skills install --agent claude")}
|
|
5756
6152
|
${chalk.cyan("rebase skills install --agent claude,cursor")}
|
|
6153
|
+
${chalk.cyan("rebase skills install --agent all")} ${chalk.gray("# scripted / CI")}
|
|
5757
6154
|
`);
|
|
5758
6155
|
}
|
|
5759
6156
|
//#endregion
|
|
@@ -5766,25 +6163,13 @@ ${chalk.green.bold("Examples")}
|
|
|
5766
6163
|
* create — Create a new API key
|
|
5767
6164
|
* revoke — Revoke an existing API key
|
|
5768
6165
|
*/
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
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
|
-
}
|
|
6166
|
+
/**
|
|
6167
|
+
* Was a hand-rolled `indexOf("=")` loop. It keyed `export KEY=value` as
|
|
6168
|
+
* `export KEY` and carried a trailing `# comment` into the value — so a key
|
|
6169
|
+
* that was present read as absent, or reached an `Authorization` header with a
|
|
6170
|
+
* comment attached and came back 401. See `readEnvFile`.
|
|
6171
|
+
*/
|
|
6172
|
+
var loadEnv = readEnvFile;
|
|
5788
6173
|
function resolveBaseUrl(env, projectRoot) {
|
|
5789
6174
|
if (env.REBASE_BASE_URL) return env.REBASE_BASE_URL;
|
|
5790
6175
|
if (projectRoot) try {
|
|
@@ -6055,7 +6440,12 @@ ${chalk.green.bold("Examples")}
|
|
|
6055
6440
|
* a documentation comment that quietly fell out of date two releases ago.
|
|
6056
6441
|
*/
|
|
6057
6442
|
async function telemetryCommand(rawArgs) {
|
|
6058
|
-
|
|
6443
|
+
const subcommand = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[0];
|
|
6444
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
6445
|
+
printHelp$2();
|
|
6446
|
+
return;
|
|
6447
|
+
}
|
|
6448
|
+
switch (subcommand) {
|
|
6059
6449
|
case "status":
|
|
6060
6450
|
case void 0:
|
|
6061
6451
|
printStatus();
|
|
@@ -6154,8 +6544,7 @@ async function loginCommand(rawArgs) {
|
|
|
6154
6544
|
const args = arg({
|
|
6155
6545
|
"--email": String,
|
|
6156
6546
|
"--password": String,
|
|
6157
|
-
"-e": "--email"
|
|
6158
|
-
"-p": "--password"
|
|
6547
|
+
"-e": "--email"
|
|
6159
6548
|
}, {
|
|
6160
6549
|
argv: rawArgs.slice(3),
|
|
6161
6550
|
permissive: true
|
|
@@ -8162,7 +8551,8 @@ async function revealEnv(rawArgs) {
|
|
|
8162
8551
|
}
|
|
8163
8552
|
async function pullEnv(rawArgs) {
|
|
8164
8553
|
const args = arg({
|
|
8165
|
-
"--
|
|
8554
|
+
"--output": String,
|
|
8555
|
+
"--out": "--output",
|
|
8166
8556
|
"--yes": Boolean,
|
|
8167
8557
|
"-y": "--yes",
|
|
8168
8558
|
"--project": String,
|
|
@@ -8174,7 +8564,7 @@ async function pullEnv(rawArgs) {
|
|
|
8174
8564
|
const { client } = await requireClient(rawArgs);
|
|
8175
8565
|
const projectId = await requireProject(rawArgs, client);
|
|
8176
8566
|
displayProjectRef(rawArgs);
|
|
8177
|
-
const outPath = path.resolve(args["--
|
|
8567
|
+
const outPath = path.resolve(args["--output"] || ".env");
|
|
8178
8568
|
try {
|
|
8179
8569
|
const list = await fetchEnvVars(client, projectId);
|
|
8180
8570
|
if (fs.existsSync(outPath)) await confirmDestructive({
|
|
@@ -10293,15 +10683,43 @@ function positionals(rawArgs) {
|
|
|
10293
10683
|
while (i < rest.length && rest[i].startsWith("-")) i++;
|
|
10294
10684
|
return rest.slice(i);
|
|
10295
10685
|
}
|
|
10686
|
+
/**
|
|
10687
|
+
* The help page for each group, keyed by every alias the dispatch below accepts.
|
|
10688
|
+
*
|
|
10689
|
+
* Aliases are listed explicitly rather than normalised first, so a group that
|
|
10690
|
+
* gains one and forgets it here degrades to the index page — wrong, but a page.
|
|
10691
|
+
* `cloud-help.test.ts` asserts the two stay in step.
|
|
10692
|
+
*
|
|
10693
|
+
* A group absent from this map has no page of its own; the index lists it.
|
|
10694
|
+
*/
|
|
10695
|
+
var GROUP_HELP = {
|
|
10696
|
+
env: printEnvHelp,
|
|
10697
|
+
domains: printDomainsHelp,
|
|
10698
|
+
domain: printDomainsHelp,
|
|
10699
|
+
extensions: printExtensionsHelp,
|
|
10700
|
+
extension: printExtensionsHelp,
|
|
10701
|
+
settings: printSettingsHelp,
|
|
10702
|
+
orgs: printOrgsHelp,
|
|
10703
|
+
org: printOrgsHelp,
|
|
10704
|
+
db: printDbHelp,
|
|
10705
|
+
database: printDbHelp,
|
|
10706
|
+
debug: printDebugHelp,
|
|
10707
|
+
storage: printStorageHelp
|
|
10708
|
+
};
|
|
10296
10709
|
async function cloudCommand(subcommand, rawArgs) {
|
|
10297
10710
|
initOutputMode(rawArgs);
|
|
10298
10711
|
const pos = positionals(rawArgs);
|
|
10299
10712
|
const group = pos[0] ?? (subcommand !== "--help" ? subcommand : void 0);
|
|
10713
|
+
const wantsHelp = rawArgs.includes("--help") || rawArgs.includes("-h");
|
|
10300
10714
|
const action = pos[1];
|
|
10301
|
-
if (!group
|
|
10715
|
+
if (!group) {
|
|
10302
10716
|
printCloudHelp();
|
|
10303
10717
|
return;
|
|
10304
10718
|
}
|
|
10719
|
+
if (wantsHelp) {
|
|
10720
|
+
(GROUP_HELP[group] ?? printCloudHelp)();
|
|
10721
|
+
return;
|
|
10722
|
+
}
|
|
10305
10723
|
switch (group) {
|
|
10306
10724
|
case "login":
|
|
10307
10725
|
await loginCommand(rawArgs);
|
|
@@ -10741,7 +11159,7 @@ async function entry(args) {
|
|
|
10741
11159
|
printHelp();
|
|
10742
11160
|
return;
|
|
10743
11161
|
}
|
|
10744
|
-
const effectiveSubcommand = parsedArgs["--help"] ? "--help" : subcommand;
|
|
11162
|
+
const effectiveSubcommand = parsedArgs["--help"] && !subcommand ? "--help" : subcommand;
|
|
10745
11163
|
switch (command) {
|
|
10746
11164
|
case "init":
|
|
10747
11165
|
await createRebaseApp(args);
|
|
@@ -10860,9 +11278,11 @@ ${chalk.green.bold("API Keys")}
|
|
|
10860
11278
|
${chalk.blue.bold("api-keys list")} List all service API keys
|
|
10861
11279
|
${chalk.blue.bold("api-keys create")} Create a new scoped API key
|
|
10862
11280
|
${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
11281
|
${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
|
|
10865
11282
|
|
|
11283
|
+
${chalk.green.bold("Usage sharing")}
|
|
11284
|
+
${chalk.blue.bold("telemetry")} Anonymous usage sharing (opt-in, off by default)
|
|
11285
|
+
|
|
10866
11286
|
${chalk.green.bold("Rebase Cloud")}
|
|
10867
11287
|
${chalk.blue.bold("cloud login")} Sign in to the hosted control plane
|
|
10868
11288
|
${chalk.blue.bold("cloud link")} Link this directory to a cloud project
|
|
@@ -10893,6 +11313,6 @@ function telemetryNotice() {
|
|
|
10893
11313
|
return chalk.gray(`Usage sharing: ${sharing ? "on" : "off"} — ${chalk.cyan("rebase telemetry")} to inspect or change\n`);
|
|
10894
11314
|
}
|
|
10895
11315
|
//#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 };
|
|
11316
|
+
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, isPortAvailable, 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
11317
|
|
|
10898
11318
|
//# sourceMappingURL=index.es.js.map
|