@rebasepro/cli 0.15.0 → 0.16.0
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/bundle.d.ts +95 -0
- package/dist/commands/cloud/resources.d.ts +36 -1
- package/dist/commands/db.d.ts +15 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/index.es.js +643 -9
- package/dist/index.es.js.map +1 -1
- package/dist/utils/libpq-url.d.ts +69 -0
- package/package.json +7 -7
- package/templates/eject/docker-compose.custom.yml +2 -2
- package/templates/overlays/baas/README.md +2 -2
- package/templates/template/.env.example +5 -2
- package/templates/template/docker-compose.yml +2 -2
package/dist/index.es.js
CHANGED
|
@@ -2423,7 +2423,7 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
2423
2423
|
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${pinnedUrl}\nDATABASE_PASSWORD=${dbPassword}`);
|
|
2424
2424
|
} else {
|
|
2425
2425
|
const dbPort = await findAvailablePort(5432);
|
|
2426
|
-
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://rebase_app:${dbPassword}@127.0.0.1:${dbPort}/rebase?options=-c%20search_path
|
|
2426
|
+
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://rebase_app:${dbPassword}@127.0.0.1:${dbPort}/rebase?options=-c%20search_path%3Dpublic&sslmode=disable\nDATABASE_PASSWORD=${dbPassword}`);
|
|
2427
2427
|
const dockerComposePath = path.join(targetDirectory, "docker-compose.yml");
|
|
2428
2428
|
if (fs.existsSync(dockerComposePath)) {
|
|
2429
2429
|
let dockerComposeContent = fs.readFileSync(dockerComposePath, "utf-8");
|
|
@@ -2827,6 +2827,75 @@ ${chalk.green.bold("introspect Options")}
|
|
|
2827
2827
|
/**
|
|
2828
2828
|
* CLI command: rebase db <action>
|
|
2829
2829
|
*/
|
|
2830
|
+
/**
|
|
2831
|
+
* A destination that names a remote store rather than a local path.
|
|
2832
|
+
*
|
|
2833
|
+
* Matched as "scheme://" generally, not as a list of the schemes we support:
|
|
2834
|
+
* an unknown scheme is still not something to join onto a filesystem path, and
|
|
2835
|
+
* a Windows drive letter ("C:\backups") has no "//" so it stays a path.
|
|
2836
|
+
*/
|
|
2837
|
+
var REMOTE_DESTINATION_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
2838
|
+
/**
|
|
2839
|
+
* Rewrite local path arguments so they mean what the user typed.
|
|
2840
|
+
*
|
|
2841
|
+
* The plugin CLI is spawned with `cwd: backendDir` — it has to be, because that
|
|
2842
|
+
* is where the plugin and its dependencies resolve from. But the developer runs
|
|
2843
|
+
* `rebase db` from the project root, so a relative `--out ./backups` was being
|
|
2844
|
+
* resolved against `backend/` and landed in `backend/backups`, while the
|
|
2845
|
+
* success line echoed the path as typed. The file was real and the reported
|
|
2846
|
+
* location was wrong, which is the worst way for a backup command to behave.
|
|
2847
|
+
* `rebase db --help` documents exactly this invocation.
|
|
2848
|
+
*
|
|
2849
|
+
* Absolutising here rather than inside the plugin keeps the fix where the cwd
|
|
2850
|
+
* is actually changed, and leaves the plugin usable on its own terms.
|
|
2851
|
+
*/
|
|
2852
|
+
function absolutizeLocalPathArgs(args, cwd) {
|
|
2853
|
+
const takesPath = (flag) => flag === "--out" || flag === "-o";
|
|
2854
|
+
/**
|
|
2855
|
+
* Flags whose next token is a value, not a positional.
|
|
2856
|
+
*
|
|
2857
|
+
* Mirrors the `arg` specs in `@rebasepro/server-postgres`'s backup CLI.
|
|
2858
|
+
* Needed only to find the `db restore` dump argument: without it,
|
|
2859
|
+
* `restore --target-db app_restored ./x.dump` treats `app_restored` as the
|
|
2860
|
+
* positional — it is the first token not starting with `-` — and turns a
|
|
2861
|
+
* database name into a path while leaving the real dump path unresolved.
|
|
2862
|
+
*/
|
|
2863
|
+
const VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
2864
|
+
"--out",
|
|
2865
|
+
"-o",
|
|
2866
|
+
"--target-db",
|
|
2867
|
+
"--exclude-schema",
|
|
2868
|
+
"--row-security-role"
|
|
2869
|
+
]);
|
|
2870
|
+
const out = [...args];
|
|
2871
|
+
for (let i = 0; i < out.length; i++) {
|
|
2872
|
+
const arg = out[i];
|
|
2873
|
+
const eq = arg.indexOf("=");
|
|
2874
|
+
if (eq > 0 && takesPath(arg.slice(0, eq))) {
|
|
2875
|
+
const value = arg.slice(eq + 1);
|
|
2876
|
+
if (value && !REMOTE_DESTINATION_RE.test(value)) out[i] = `${arg.slice(0, eq)}=${path.resolve(cwd, value)}`;
|
|
2877
|
+
continue;
|
|
2878
|
+
}
|
|
2879
|
+
if (takesPath(arg)) {
|
|
2880
|
+
const value = out[i + 1];
|
|
2881
|
+
if (value && !value.startsWith("-") && !REMOTE_DESTINATION_RE.test(value)) {
|
|
2882
|
+
out[i + 1] = path.resolve(cwd, value);
|
|
2883
|
+
i++;
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
const restoreAt = out.indexOf("restore");
|
|
2888
|
+
if (restoreAt !== -1) for (let i = restoreAt + 1; i < out.length; i++) {
|
|
2889
|
+
const arg = out[i];
|
|
2890
|
+
if (arg.startsWith("-")) {
|
|
2891
|
+
if (!arg.includes("=") && VALUE_FLAGS.has(arg)) i++;
|
|
2892
|
+
continue;
|
|
2893
|
+
}
|
|
2894
|
+
if (!REMOTE_DESTINATION_RE.test(arg)) out[i] = path.resolve(cwd, arg);
|
|
2895
|
+
break;
|
|
2896
|
+
}
|
|
2897
|
+
return out;
|
|
2898
|
+
}
|
|
2830
2899
|
async function dbCommand(subcommand, rawArgs) {
|
|
2831
2900
|
if (!subcommand || subcommand === "--help") {
|
|
2832
2901
|
printDbHelp$1();
|
|
@@ -2849,6 +2918,7 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
2849
2918
|
const envFile = findEnvFile(projectRoot);
|
|
2850
2919
|
const env = { ...process.env };
|
|
2851
2920
|
if (envFile) env.DOTENV_CONFIG_PATH = envFile;
|
|
2921
|
+
const childArgs = absolutizeLocalPathArgs(rawArgs.slice(2), process.cwd());
|
|
2852
2922
|
try {
|
|
2853
2923
|
if (pluginCli.endsWith(".ts")) {
|
|
2854
2924
|
const tsxBin = resolveTsx(projectRoot);
|
|
@@ -2856,12 +2926,12 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
2856
2926
|
console.error(chalk.red("✗ Could not find tsx binary."));
|
|
2857
2927
|
process.exit(1);
|
|
2858
2928
|
}
|
|
2859
|
-
await execa(tsxBin, [pluginCli, ...
|
|
2929
|
+
await execa(tsxBin, [pluginCli, ...childArgs], {
|
|
2860
2930
|
cwd: backendDir,
|
|
2861
2931
|
stdio: "inherit",
|
|
2862
2932
|
env
|
|
2863
2933
|
});
|
|
2864
|
-
} else await execa("node", [pluginCli, ...
|
|
2934
|
+
} else await execa("node", [pluginCli, ...childArgs], {
|
|
2865
2935
|
cwd: backendDir,
|
|
2866
2936
|
stdio: "inherit",
|
|
2867
2937
|
env
|
|
@@ -4963,10 +5033,165 @@ async function buildBundle(options) {
|
|
|
4963
5033
|
type: "module",
|
|
4964
5034
|
dependencies: declared
|
|
4965
5035
|
}, null, 2)}\n`, "utf8");
|
|
5036
|
+
const vendor = vendorDependencies({
|
|
5037
|
+
outDir,
|
|
5038
|
+
declared,
|
|
5039
|
+
nativeModules,
|
|
5040
|
+
required: [getActiveBackendPlugin(path.join(projectRoot, "backend"))].filter((name) => Boolean(name)),
|
|
5041
|
+
requested: options.vendor
|
|
5042
|
+
});
|
|
5043
|
+
if (vendor.vendored) {
|
|
5044
|
+
manifest.deps.vendored = true;
|
|
5045
|
+
manifest.deps.vendorTarget = vendor.target;
|
|
5046
|
+
fs.writeFileSync(path.join(outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
5047
|
+
}
|
|
4966
5048
|
return {
|
|
4967
5049
|
outDir,
|
|
4968
5050
|
manifest,
|
|
4969
|
-
collectionCount: collections.length
|
|
5051
|
+
collectionCount: collections.length,
|
|
5052
|
+
vendor
|
|
5053
|
+
};
|
|
5054
|
+
}
|
|
5055
|
+
/** Default install target: what the published runtime image runs. */
|
|
5056
|
+
var VENDOR_TARGET_OS = "linux";
|
|
5057
|
+
var VENDOR_TARGET_CPU = "x64";
|
|
5058
|
+
/**
|
|
5059
|
+
* Where a vendored bundle starts being too big to upload.
|
|
5060
|
+
*
|
|
5061
|
+
* The control plane refuses a bundle over 100 MB, and that ceiling is not
|
|
5062
|
+
* arbitrary or easily raised: its pod has a 512Mi memory limit and the upload
|
|
5063
|
+
* route holds the body while it writes it, so the cap protects the process that
|
|
5064
|
+
* also serves the console, deploys and billing. Vendoring is the one change that
|
|
5065
|
+
* can push a bundle near it.
|
|
5066
|
+
*
|
|
5067
|
+
* So the warning is here, at build time, where the remedy is one flag away —
|
|
5068
|
+
* rather than at deploy time as a 413 nobody can act on without rebuilding. The
|
|
5069
|
+
* threshold sits below the real cap because this measures the tree on disk and
|
|
5070
|
+
* the upload is compressed: crossing it means "getting close", not "will fail".
|
|
5071
|
+
*/
|
|
5072
|
+
var VENDOR_SIZE_WARN_BYTES = 150 * 1024 * 1024;
|
|
5073
|
+
/** Bytes on disk under `dir`. Bounded so a pathological tree cannot hang a build. */
|
|
5074
|
+
function directorySize(dir, budget = 2e5) {
|
|
5075
|
+
let total = 0;
|
|
5076
|
+
let visited = 0;
|
|
5077
|
+
const stack = [dir];
|
|
5078
|
+
while (stack.length > 0 && visited < budget) {
|
|
5079
|
+
const current = stack.pop();
|
|
5080
|
+
let entries;
|
|
5081
|
+
try {
|
|
5082
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
5083
|
+
} catch {
|
|
5084
|
+
continue;
|
|
5085
|
+
}
|
|
5086
|
+
for (const entry of entries) {
|
|
5087
|
+
visited++;
|
|
5088
|
+
const full = path.join(current, entry.name);
|
|
5089
|
+
if (entry.isDirectory()) stack.push(full);
|
|
5090
|
+
else if (entry.isFile()) try {
|
|
5091
|
+
total += fs.statSync(full).size;
|
|
5092
|
+
} catch {}
|
|
5093
|
+
}
|
|
5094
|
+
}
|
|
5095
|
+
return total;
|
|
5096
|
+
}
|
|
5097
|
+
/**
|
|
5098
|
+
* Install the bundle's declared dependencies into the bundle itself.
|
|
5099
|
+
*
|
|
5100
|
+
* ## What this buys
|
|
5101
|
+
*
|
|
5102
|
+
* A managed pod's bundle lives on an `emptyDir`, so it is re-fetched and
|
|
5103
|
+
* re-installed on **every** start — an eviction, a node failure, an OOM, a
|
|
5104
|
+
* runtime rollout. The install is 35–55 seconds of a 40–60 second cold start,
|
|
5105
|
+
* which makes it the price of every unplanned restart a tenant suffers. Doing it
|
|
5106
|
+
* once at build time instead of every time at boot takes that to roughly the
|
|
5107
|
+
* cost of untarring.
|
|
5108
|
+
*
|
|
5109
|
+
* The pod side needs no change to benefit: the init container already skips
|
|
5110
|
+
* installing when `node_modules` is present, a guard that existed for
|
|
5111
|
+
* pre-baked images and turns out to be exactly the hook this needs.
|
|
5112
|
+
*
|
|
5113
|
+
* ## Why it refuses to vendor native code
|
|
5114
|
+
*
|
|
5115
|
+
* A compiled binary is valid only for the platform it was built for, and a
|
|
5116
|
+
* developer's machine is rarely the deployment's. The managed runtime already
|
|
5117
|
+
* refuses bundles containing native modules for the same reason, so this refusal
|
|
5118
|
+
* costs nothing there — but a self-hosted project may legitimately use them, and
|
|
5119
|
+
* for those the honest answer is to install in the container, where the platform
|
|
5120
|
+
* is known.
|
|
5121
|
+
*
|
|
5122
|
+
* ## Why `--os` and `--cpu` are not optional
|
|
5123
|
+
*
|
|
5124
|
+
* The dangerous case is not native code, which is detectable. It is a pure-JS
|
|
5125
|
+
* package whose real work lives in a **platform-specific optional dependency** —
|
|
5126
|
+
* `esbuild` being the one everybody meets. Installing on an Apple Silicon Mac
|
|
5127
|
+
* resolves `@esbuild/darwin-arm64`, produces a tree that looks complete, and
|
|
5128
|
+
* fails at import inside a linux/amd64 pod. npm resolves optional dependencies
|
|
5129
|
+
* for the declared target rather than the host when told to, so it is told to.
|
|
5130
|
+
*/
|
|
5131
|
+
function vendorDependencies(options) {
|
|
5132
|
+
if (options.requested === false) return {
|
|
5133
|
+
vendored: false,
|
|
5134
|
+
skipped: "disabled with --no-vendor"
|
|
5135
|
+
};
|
|
5136
|
+
if (Object.keys(options.declared).length === 0) return {
|
|
5137
|
+
vendored: false,
|
|
5138
|
+
skipped: "the bundle declares no dependencies"
|
|
5139
|
+
};
|
|
5140
|
+
if (options.nativeModules.length > 0) return {
|
|
5141
|
+
vendored: false,
|
|
5142
|
+
skipped: `the dependency closure contains native code (${options.nativeModules.map((m) => m.name).join(", ")}), which is only valid on the platform it was compiled for`
|
|
5143
|
+
};
|
|
5144
|
+
const target = {
|
|
5145
|
+
os: VENDOR_TARGET_OS,
|
|
5146
|
+
cpu: "x64",
|
|
5147
|
+
node: process.versions.node.split(".")[0]
|
|
5148
|
+
};
|
|
5149
|
+
const run = options.run ?? ((cmd, args, cwd) => {
|
|
5150
|
+
const result = spawnSync(cmd, args, {
|
|
5151
|
+
cwd,
|
|
5152
|
+
stdio: "pipe",
|
|
5153
|
+
encoding: "utf8"
|
|
5154
|
+
});
|
|
5155
|
+
if (result.error) throw result.error;
|
|
5156
|
+
if (result.status !== 0) throw new Error(`${cmd} ${args.join(" ")} exited ${result.status}\n${result.stderr || result.stdout || ""}`.trim());
|
|
5157
|
+
});
|
|
5158
|
+
try {
|
|
5159
|
+
run("npm", [
|
|
5160
|
+
"install",
|
|
5161
|
+
"--omit=dev",
|
|
5162
|
+
"--ignore-scripts",
|
|
5163
|
+
"--no-audit",
|
|
5164
|
+
"--no-fund",
|
|
5165
|
+
`--os=${target.os}`,
|
|
5166
|
+
`--cpu=${target.cpu}`
|
|
5167
|
+
], options.outDir);
|
|
5168
|
+
} catch (error) {
|
|
5169
|
+
return {
|
|
5170
|
+
vendored: false,
|
|
5171
|
+
skipped: `npm install failed: ${error instanceof Error ? error.message : String(error)}`
|
|
5172
|
+
};
|
|
5173
|
+
}
|
|
5174
|
+
const installed = path.join(options.outDir, "node_modules");
|
|
5175
|
+
if (!fs.existsSync(installed)) return {
|
|
5176
|
+
vendored: false,
|
|
5177
|
+
skipped: "npm install produced no node_modules"
|
|
5178
|
+
};
|
|
5179
|
+
const missing = (options.required ?? []).filter((name) => !fs.existsSync(path.join(installed, ...name.split("/"), "package.json")));
|
|
5180
|
+
if (missing.length > 0) {
|
|
5181
|
+
fs.rmSync(installed, {
|
|
5182
|
+
recursive: true,
|
|
5183
|
+
force: true
|
|
5184
|
+
});
|
|
5185
|
+
fs.rmSync(path.join(options.outDir, "package-lock.json"), { force: true });
|
|
5186
|
+
return {
|
|
5187
|
+
vendored: false,
|
|
5188
|
+
skipped: `the installed tree is missing ${missing.join(", ")}, which the runtime resolves from the bundle — the bundle declares it at a version no registry can serve (a workspace link), so nothing was vendored rather than shipping a tree that boots without a driver`
|
|
5189
|
+
};
|
|
5190
|
+
}
|
|
5191
|
+
return {
|
|
5192
|
+
vendored: true,
|
|
5193
|
+
target,
|
|
5194
|
+
bytes: directorySize(installed)
|
|
4970
5195
|
};
|
|
4971
5196
|
}
|
|
4972
5197
|
/**
|
|
@@ -5374,6 +5599,8 @@ ${chalk.bold("Options")}
|
|
|
5374
5599
|
--out <dir> Bundle output directory (default: ${DEFAULT_BUNDLE_DIR})
|
|
5375
5600
|
--skip-type-check Compile without type checking (faster; use for iteration only)
|
|
5376
5601
|
--skip-schema Do not regenerate the database schema from collections
|
|
5602
|
+
--no-vendor Do not install dependencies into the bundle (they
|
|
5603
|
+
install on every pod start instead, ~40-60s slower)
|
|
5377
5604
|
--legacy Run every workspace's own build script instead
|
|
5378
5605
|
-h, --help Show this help
|
|
5379
5606
|
|
|
@@ -5393,6 +5620,7 @@ async function buildCommand(rawArgs = []) {
|
|
|
5393
5620
|
"--output": String,
|
|
5394
5621
|
"--out": "--output",
|
|
5395
5622
|
"--skip-type-check": Boolean,
|
|
5623
|
+
"--no-vendor": Boolean,
|
|
5396
5624
|
"--skip-schema": Boolean,
|
|
5397
5625
|
"--no-static": Boolean,
|
|
5398
5626
|
"--skip-static-build": Boolean,
|
|
@@ -5457,11 +5685,24 @@ async function buildCommand(rawArgs = []) {
|
|
|
5457
5685
|
runtimeRange: manifest.rebase,
|
|
5458
5686
|
storage: manifest.storage,
|
|
5459
5687
|
skipTypeCheck: args["--skip-type-check"],
|
|
5460
|
-
skipSchema: args["--skip-schema"]
|
|
5688
|
+
skipSchema: args["--skip-schema"],
|
|
5689
|
+
vendor: args["--no-vendor"] ? false : void 0
|
|
5461
5690
|
});
|
|
5462
5691
|
const rel = path.relative(projectRoot, result.outDir);
|
|
5463
5692
|
console.log(chalk.green(` ✓ bundle → ${rel}/`));
|
|
5464
5693
|
console.log(chalk.dim(` ${result.collectionCount} collection(s), schema ${result.manifest.schemaVersion}`));
|
|
5694
|
+
if (result.vendor.vendored) {
|
|
5695
|
+
console.log(chalk.dim(` dependencies installed into the bundle (${result.vendor.target?.os}/${result.vendor.target?.cpu})`));
|
|
5696
|
+
if ((result.vendor.bytes ?? 0) > 157286400) {
|
|
5697
|
+
const mb = Math.round((result.vendor.bytes ?? 0) / (1024 * 1024));
|
|
5698
|
+
console.log(chalk.yellow(` ⚠ the installed tree is ${mb} MB — close to the 100 MB upload limit`));
|
|
5699
|
+
console.log(chalk.dim(" Rebuild with --no-vendor if the deploy is rejected as too large."));
|
|
5700
|
+
}
|
|
5701
|
+
} else if (Object.keys(result.manifest.deps.declared).length === 0) console.log(chalk.dim(" no dependencies to install — nothing to bundle"));
|
|
5702
|
+
else {
|
|
5703
|
+
console.log(chalk.yellow(` ⚠ dependencies not bundled: ${result.vendor.skipped}`));
|
|
5704
|
+
console.log(chalk.dim(" They install on every pod start instead, which is ~40-60s of cold start."));
|
|
5705
|
+
}
|
|
5465
5706
|
if (result.manifest.hooks.native) {
|
|
5466
5707
|
const names = (result.manifest.hooks.nativeModules ?? []).map((m) => m.name).join(", ");
|
|
5467
5708
|
console.log(chalk.yellow(` ⚠ native dependencies detected: ${names}`));
|
|
@@ -6312,6 +6553,92 @@ ${chalk.green.bold("Examples")}
|
|
|
6312
6553
|
`);
|
|
6313
6554
|
}
|
|
6314
6555
|
//#endregion
|
|
6556
|
+
//#region src/utils/libpq-url.ts
|
|
6557
|
+
/** Postgres URI schemes. A key/value DSN ("host=… dbname=…") is not one. */
|
|
6558
|
+
var POSTGRES_URI_RE = /^postgres(ql)?:\/\//i;
|
|
6559
|
+
/**
|
|
6560
|
+
* Return the query parameters libpq would reject, or an empty array.
|
|
6561
|
+
*
|
|
6562
|
+
* Deliberately not built on `new URL()`: that parser is happy to accept the
|
|
6563
|
+
* broken form (it splits on the first `=` and keeps the rest as the value),
|
|
6564
|
+
* so it cannot see the defect at all. The rule being checked is libpq's, and
|
|
6565
|
+
* it is about the raw text.
|
|
6566
|
+
*/
|
|
6567
|
+
function findUnparseableParams(connectionString) {
|
|
6568
|
+
if (!POSTGRES_URI_RE.test(connectionString)) return [];
|
|
6569
|
+
const q = connectionString.indexOf("?");
|
|
6570
|
+
if (q === -1) return [];
|
|
6571
|
+
const query = connectionString.slice(q + 1).split("#")[0];
|
|
6572
|
+
if (!query) return [];
|
|
6573
|
+
const found = [];
|
|
6574
|
+
for (const part of query.split("&")) {
|
|
6575
|
+
if (!part) continue;
|
|
6576
|
+
if (part.split("=").length > 2) found.push({
|
|
6577
|
+
name: part.slice(0, part.indexOf("=")),
|
|
6578
|
+
raw: part
|
|
6579
|
+
});
|
|
6580
|
+
}
|
|
6581
|
+
return found;
|
|
6582
|
+
}
|
|
6583
|
+
/**
|
|
6584
|
+
* Percent-encode the offending `=` characters, leaving everything else byte for
|
|
6585
|
+
* byte as it was.
|
|
6586
|
+
*
|
|
6587
|
+
* Only the second and later `=` in a parameter are rewritten — the first is the
|
|
6588
|
+
* real separator. Nothing else is touched: re-serialising through `URL` would
|
|
6589
|
+
* also reorder parameters and turn spaces into `+`, which libpq does not decode,
|
|
6590
|
+
* so a "tidy up" would trade one unparseable string for another.
|
|
6591
|
+
*/
|
|
6592
|
+
function encodeExtraEquals(connectionString) {
|
|
6593
|
+
if (findUnparseableParams(connectionString).length === 0) return connectionString;
|
|
6594
|
+
const q = connectionString.indexOf("?");
|
|
6595
|
+
const head = connectionString.slice(0, q + 1);
|
|
6596
|
+
const rest = connectionString.slice(q + 1);
|
|
6597
|
+
const hash = rest.indexOf("#");
|
|
6598
|
+
const query = hash === -1 ? rest : rest.slice(0, hash);
|
|
6599
|
+
const fragment = hash === -1 ? "" : rest.slice(hash);
|
|
6600
|
+
return `${head}${query.split("&").map((part) => {
|
|
6601
|
+
const first = part.indexOf("=");
|
|
6602
|
+
if (first === -1) return part;
|
|
6603
|
+
return `${part.slice(0, first)}=${part.slice(first + 1).replace(/=/g, "%3D")}`;
|
|
6604
|
+
}).join("&")}${fragment}`;
|
|
6605
|
+
}
|
|
6606
|
+
/** Variables whose value is a Postgres connection string. */
|
|
6607
|
+
var CONNECTION_VARIABLES = ["DATABASE_URL", "ADMIN_CONNECTION_STRING"];
|
|
6608
|
+
/**
|
|
6609
|
+
* Scan one file's text for connection strings libpq would reject.
|
|
6610
|
+
*
|
|
6611
|
+
* Handles both shapes a scaffolded project uses, because both shipped with the
|
|
6612
|
+
* defect and a deployed stack is broken by the compose one alone:
|
|
6613
|
+
*
|
|
6614
|
+
* .env DATABASE_URL=postgresql://…
|
|
6615
|
+
* docker-compose.yml DATABASE_URL: postgresql://…
|
|
6616
|
+
*/
|
|
6617
|
+
function scanTextForLibpqUrls(file, text) {
|
|
6618
|
+
const findings = [];
|
|
6619
|
+
for (const line of text.split("\n")) {
|
|
6620
|
+
const trimmed = line.trim();
|
|
6621
|
+
if (trimmed.startsWith("#")) continue;
|
|
6622
|
+
for (const variable of CONNECTION_VARIABLES) {
|
|
6623
|
+
const match = trimmed.match(new RegExp(`^(?:export\\s+)?${variable}\\s*[:=]\\s*(.+)$`));
|
|
6624
|
+
if (!match) continue;
|
|
6625
|
+
let value = match[1].trim();
|
|
6626
|
+
const quoted = value.match(/^(['"])(.*)\1$/);
|
|
6627
|
+
if (quoted) value = quoted[2];
|
|
6628
|
+
if (!value) continue;
|
|
6629
|
+
const params = findUnparseableParams(value);
|
|
6630
|
+
if (params.length === 0) continue;
|
|
6631
|
+
findings.push({
|
|
6632
|
+
file,
|
|
6633
|
+
variable,
|
|
6634
|
+
params: params.map((p) => p.name),
|
|
6635
|
+
suggested: encodeExtraEquals(value)
|
|
6636
|
+
});
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
6639
|
+
return findings;
|
|
6640
|
+
}
|
|
6641
|
+
//#endregion
|
|
6315
6642
|
//#region src/commands/doctor.ts
|
|
6316
6643
|
/**
|
|
6317
6644
|
* CLI command: rebase doctor
|
|
@@ -6320,6 +6647,76 @@ ${chalk.green.bold("Examples")}
|
|
|
6320
6647
|
* the generated Drizzle schema, and the live PostgreSQL database.
|
|
6321
6648
|
*/
|
|
6322
6649
|
/**
|
|
6650
|
+
* Files that can hold a connection string the project actually runs on.
|
|
6651
|
+
*
|
|
6652
|
+
* `.env` is what local commands read; the compose files are what a self-hosted
|
|
6653
|
+
* stack and its scheduled backup cron read. Both shipped with the defect, and a
|
|
6654
|
+
* deployed stack is broken by the compose one even when `.env` has been fixed.
|
|
6655
|
+
* `.env.example` is deliberately absent: nothing runs on it.
|
|
6656
|
+
*/
|
|
6657
|
+
var CONNECTION_FILES = [
|
|
6658
|
+
".env",
|
|
6659
|
+
".env.local",
|
|
6660
|
+
"docker-compose.yml",
|
|
6661
|
+
"docker-compose.yaml",
|
|
6662
|
+
"docker-compose.custom.yml"
|
|
6663
|
+
];
|
|
6664
|
+
/**
|
|
6665
|
+
* Find connection strings libpq cannot parse, anywhere in the project.
|
|
6666
|
+
*
|
|
6667
|
+
* Exported for the tests; `envFile` is passed separately because a project may
|
|
6668
|
+
* keep its `.env` outside the root (see `findEnvFile`).
|
|
6669
|
+
*/
|
|
6670
|
+
function findLibpqUrlProblems(projectRoot, envFile) {
|
|
6671
|
+
const candidates = CONNECTION_FILES.map((f) => path.join(projectRoot, f));
|
|
6672
|
+
if (envFile) candidates.push(envFile);
|
|
6673
|
+
const findings = [];
|
|
6674
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6675
|
+
for (const file of candidates) {
|
|
6676
|
+
if (seen.has(file)) continue;
|
|
6677
|
+
seen.add(file);
|
|
6678
|
+
let text;
|
|
6679
|
+
try {
|
|
6680
|
+
if (!fs.existsSync(file)) continue;
|
|
6681
|
+
text = fs.readFileSync(file, "utf-8");
|
|
6682
|
+
} catch {
|
|
6683
|
+
continue;
|
|
6684
|
+
}
|
|
6685
|
+
findings.push(...scanTextForLibpqUrls(path.relative(projectRoot, file) || path.basename(file), text));
|
|
6686
|
+
}
|
|
6687
|
+
return findings;
|
|
6688
|
+
}
|
|
6689
|
+
/**
|
|
6690
|
+
* Report unparseable connection strings, if any.
|
|
6691
|
+
*
|
|
6692
|
+
* Runs before the plugin's drift check and never blocks it. The plugin connects
|
|
6693
|
+
* through node-postgres, which parses these URLs happily — so it cannot see this
|
|
6694
|
+
* defect, and a project with it will otherwise get a clean bill of health while
|
|
6695
|
+
* `rebase db backup` fails.
|
|
6696
|
+
*/
|
|
6697
|
+
function reportLibpqUrlProblems(findings) {
|
|
6698
|
+
if (findings.length === 0) return;
|
|
6699
|
+
console.log("");
|
|
6700
|
+
console.log(chalk.red.bold(" ✗ Connection string that PostgreSQL's own tools cannot parse"));
|
|
6701
|
+
console.log("");
|
|
6702
|
+
for (const f of findings) {
|
|
6703
|
+
console.log(` ${chalk.bold(f.file)} → ${chalk.bold(f.variable)}`);
|
|
6704
|
+
console.log(chalk.gray(` the "${f.params.join("\", \"")}" parameter contains an unencoded "=".`));
|
|
6705
|
+
console.log(chalk.gray(" Replace the value with:"));
|
|
6706
|
+
console.log(` ${chalk.cyan(f.suggested)}`);
|
|
6707
|
+
console.log("");
|
|
6708
|
+
}
|
|
6709
|
+
console.log(chalk.gray(" libpq splits a query parameter on the first \"=\" and rejects any further"));
|
|
6710
|
+
console.log(chalk.gray(" one, so this fails:"));
|
|
6711
|
+
console.log(chalk.gray(" extra key/value separator \"=\" in URI query parameter"));
|
|
6712
|
+
console.log("");
|
|
6713
|
+
console.log(chalk.gray(" Affects rebase db backup / restore, scheduled backups, and psql."));
|
|
6714
|
+
console.log(chalk.gray(" NOT rebase dev or db push — those use a driver that accepts it, which"));
|
|
6715
|
+
console.log(chalk.gray(" is why a project can look healthy and still have no working backups."));
|
|
6716
|
+
console.log(chalk.gray(" Projects scaffolded before 2026-08-18 all carry it."));
|
|
6717
|
+
console.log("");
|
|
6718
|
+
}
|
|
6719
|
+
/**
|
|
6323
6720
|
* `--help` is answered before the project guard, not after.
|
|
6324
6721
|
*
|
|
6325
6722
|
* `doctor` declared no `--help` at all, so the flag fell through to the command
|
|
@@ -6330,7 +6727,7 @@ ${chalk.green.bold("Examples")}
|
|
|
6330
6727
|
*/
|
|
6331
6728
|
function printDoctorHelp() {
|
|
6332
6729
|
console.log(`
|
|
6333
|
-
${chalk.bold("rebase doctor")} —
|
|
6730
|
+
${chalk.bold("rebase doctor")} — Check a project for drift and misconfiguration
|
|
6334
6731
|
|
|
6335
6732
|
${chalk.green.bold("Usage")}
|
|
6336
6733
|
rebase doctor
|
|
@@ -6338,6 +6735,10 @@ ${chalk.green.bold("Usage")}
|
|
|
6338
6735
|
Compares the collections you declare, the generated Drizzle schema, and the
|
|
6339
6736
|
tables that actually exist, then reports what disagrees and how to reconcile it.
|
|
6340
6737
|
|
|
6738
|
+
Also checks the connection strings in .env and the compose files for the
|
|
6739
|
+
unencoded "=" that makes PostgreSQL's own tools refuse to parse them — which
|
|
6740
|
+
breaks backups and psql while leaving the app itself working.
|
|
6741
|
+
|
|
6341
6742
|
Run from inside a Rebase project — it reads the project's collections and
|
|
6342
6743
|
connects to its database.
|
|
6343
6744
|
`);
|
|
@@ -6363,6 +6764,7 @@ async function doctorCommand(rawArgs) {
|
|
|
6363
6764
|
const envFile = findEnvFile(projectRoot);
|
|
6364
6765
|
const env = { ...process.env };
|
|
6365
6766
|
if (envFile) env.DOTENV_CONFIG_PATH = envFile;
|
|
6767
|
+
reportLibpqUrlProblems(findLibpqUrlProblems(projectRoot, envFile));
|
|
6366
6768
|
try {
|
|
6367
6769
|
if (pluginCli.endsWith(".ts")) {
|
|
6368
6770
|
const tsxBin = resolveTsx(projectRoot);
|
|
@@ -11521,7 +11923,115 @@ async function storageAttachCommand(rawArgs) {
|
|
|
11521
11923
|
reportError(e, "Failed to attach storage");
|
|
11522
11924
|
}
|
|
11523
11925
|
}
|
|
11524
|
-
|
|
11926
|
+
/**
|
|
11927
|
+
* `rebase cloud clusters` — list, register and verify the clusters tenants run on.
|
|
11928
|
+
*
|
|
11929
|
+
* Registration is deliberately an operator action: the `clusters` collection is
|
|
11930
|
+
* admin-only, and a cluster record carries a credential with enough power to
|
|
11931
|
+
* create namespaces and read every secret in them. A self-serve "bring your own
|
|
11932
|
+
* cluster" flow is a different feature with a different threat model.
|
|
11933
|
+
*/
|
|
11934
|
+
async function clustersCommand(action, rawArgs) {
|
|
11935
|
+
if (action === "verify") return clustersVerifyCommand(rawArgs);
|
|
11936
|
+
if (action === "add") return clustersAddCommand(rawArgs);
|
|
11937
|
+
return clustersListCommand(rawArgs);
|
|
11938
|
+
}
|
|
11939
|
+
/**
|
|
11940
|
+
* Ask a registered cluster whether it can actually host a tenant.
|
|
11941
|
+
*
|
|
11942
|
+
* The question this exists to answer early is the one that otherwise gets
|
|
11943
|
+
* answered by a customer's first deploy failing halfway through provisioning,
|
|
11944
|
+
* with an error they cannot act on and half a tenant already created.
|
|
11945
|
+
*/
|
|
11946
|
+
async function clustersVerifyCommand(rawArgs) {
|
|
11947
|
+
const { client } = await requireClient(rawArgs);
|
|
11948
|
+
const id = rawArgs.find((a) => !a.startsWith("--") && a !== "clusters" && a !== "verify");
|
|
11949
|
+
if (!id) fail("Usage: rebase cloud clusters verify <cluster-id> [--baseline]", void 0, "bad_request");
|
|
11950
|
+
const withBaseline = rawArgs.includes("--baseline");
|
|
11951
|
+
let report;
|
|
11952
|
+
try {
|
|
11953
|
+
report = await client.functions.invoke("cluster-baseline", void 0, {
|
|
11954
|
+
method: "GET",
|
|
11955
|
+
path: `verify/${id}${withBaseline ? "?baseline=1" : ""}`
|
|
11956
|
+
});
|
|
11957
|
+
} catch (error) {
|
|
11958
|
+
reportError(error, "Could not verify the cluster");
|
|
11959
|
+
return;
|
|
11960
|
+
}
|
|
11961
|
+
emit(() => {
|
|
11962
|
+
console.log("");
|
|
11963
|
+
const tone = report.verdict === "ready" ? chalk.green : report.verdict === "degraded" ? chalk.yellow : chalk.red;
|
|
11964
|
+
console.log(` ${tone(chalk.bold(report.verdict.toUpperCase()))} ${chalk.gray(String(id))}`);
|
|
11965
|
+
console.log("");
|
|
11966
|
+
keyValues([["Reachable", report.reachable ? "yes" : chalk.red("no")], ["Permissions", `${report.permissions.allowed.length} allowed, ${report.permissions.denied.length} denied`]]);
|
|
11967
|
+
if (report.blockers.length > 0) {
|
|
11968
|
+
console.log("");
|
|
11969
|
+
for (const b of report.blockers) console.log(` ${chalk.red("✗")} ${b}`);
|
|
11970
|
+
}
|
|
11971
|
+
console.log("");
|
|
11972
|
+
if (!withBaseline && report.verdict !== "unusable") {
|
|
11973
|
+
note("Add --baseline to also check ingress-nginx, cert-manager and CloudNativePG.");
|
|
11974
|
+
console.log("");
|
|
11975
|
+
}
|
|
11976
|
+
}, () => report);
|
|
11977
|
+
if (report.verdict === "unusable") process.exitCode = 1;
|
|
11978
|
+
}
|
|
11979
|
+
/**
|
|
11980
|
+
* Register a cluster from a kubeconfig file.
|
|
11981
|
+
*
|
|
11982
|
+
* Verifies immediately rather than reporting a successful insert: a row that
|
|
11983
|
+
* names an unreachable cluster is worse than no row, because a project pointed
|
|
11984
|
+
* at it fails at deploy instead of at registration.
|
|
11985
|
+
*/
|
|
11986
|
+
async function clustersAddCommand(rawArgs) {
|
|
11987
|
+
const { client } = await requireClient(rawArgs);
|
|
11988
|
+
const flag = (name) => {
|
|
11989
|
+
const i = rawArgs.indexOf(name);
|
|
11990
|
+
return i === -1 ? void 0 : rawArgs[i + 1];
|
|
11991
|
+
};
|
|
11992
|
+
const name = flag("--name");
|
|
11993
|
+
const provider = flag("--provider");
|
|
11994
|
+
const region = flag("--region");
|
|
11995
|
+
const kubeconfigPath = flag("--kubeconfig");
|
|
11996
|
+
if (!name || !provider || !region || !kubeconfigPath) fail("Usage: rebase cloud clusters add --name <n> --provider <gcp|aws|hetzner> --region <r> --kubeconfig <path>", void 0, "bad_request");
|
|
11997
|
+
if (![
|
|
11998
|
+
"gcp",
|
|
11999
|
+
"aws",
|
|
12000
|
+
"hetzner"
|
|
12001
|
+
].includes(provider)) fail(`--provider must be gcp, aws or hetzner (got "${provider}")`, void 0, "bad_request");
|
|
12002
|
+
let kubeConfigData;
|
|
12003
|
+
try {
|
|
12004
|
+
kubeConfigData = fs.readFileSync(kubeconfigPath, "utf8");
|
|
12005
|
+
} catch {
|
|
12006
|
+
fail(`Could not read ${kubeconfigPath}`, void 0, "bad_request");
|
|
12007
|
+
return;
|
|
12008
|
+
}
|
|
12009
|
+
let created;
|
|
12010
|
+
try {
|
|
12011
|
+
created = await client.data.collection("clusters").create({
|
|
12012
|
+
name,
|
|
12013
|
+
provider,
|
|
12014
|
+
region,
|
|
12015
|
+
authType: "kubeconfig",
|
|
12016
|
+
kubeConfigData
|
|
12017
|
+
});
|
|
12018
|
+
} catch (error) {
|
|
12019
|
+
reportError(error, "Could not register the cluster");
|
|
12020
|
+
return;
|
|
12021
|
+
}
|
|
12022
|
+
emit(() => {
|
|
12023
|
+
success(`Registered ${name} (${created.id}).`);
|
|
12024
|
+
noteBlank();
|
|
12025
|
+
note("Verifying it can host tenants:");
|
|
12026
|
+
note(chalk.cyan(` rebase cloud clusters verify ${created.id} --baseline`));
|
|
12027
|
+
}, () => ({
|
|
12028
|
+
id: created.id,
|
|
12029
|
+
name,
|
|
12030
|
+
provider,
|
|
12031
|
+
region
|
|
12032
|
+
}));
|
|
12033
|
+
}
|
|
12034
|
+
async function clustersListCommand(rawArgs) {
|
|
11525
12035
|
const { client } = await requireClient(rawArgs);
|
|
11526
12036
|
try {
|
|
11527
12037
|
const clusters = (await client.data.collection("clusters").find({ limit: 100 })).data;
|
|
@@ -11677,6 +12187,122 @@ async function billingCommand(rawArgs) {
|
|
|
11677
12187
|
reportError(e, "Failed to load billing");
|
|
11678
12188
|
}
|
|
11679
12189
|
}
|
|
12190
|
+
/** The dials, and the flag that sets each. */
|
|
12191
|
+
var DIAL_FLAGS = {
|
|
12192
|
+
"--cpu": "cpu",
|
|
12193
|
+
"--memory": "memory",
|
|
12194
|
+
"--db-mode": "databaseMode",
|
|
12195
|
+
"--db-instances": "databaseInstances",
|
|
12196
|
+
"--db-cpu": "databaseCpu",
|
|
12197
|
+
"--db-memory": "databaseMemory"
|
|
12198
|
+
};
|
|
12199
|
+
/**
|
|
12200
|
+
* `rebase cloud resources` — show what a project is given, and change it.
|
|
12201
|
+
*
|
|
12202
|
+
* ## Why nothing is validated here
|
|
12203
|
+
*
|
|
12204
|
+
* The rules are the *target cluster's*, not the CLI's: GKE Autopilot bills a
|
|
12205
|
+
* 250m/512Mi floor and rewrites anything outside a 1:1–6.5:1 memory:CPU band,
|
|
12206
|
+
* while a Hetzner or EKS node has neither constraint. A CLI that carried those
|
|
12207
|
+
* numbers would be wrong for two of the three providers the moment it shipped,
|
|
12208
|
+
* and would drift from the control plane the first time either changed.
|
|
12209
|
+
*
|
|
12210
|
+
* So the control plane validates and this reports what it said. The same
|
|
12211
|
+
* boundary refuses a raw PATCH and a console save, which is the property worth
|
|
12212
|
+
* having — a check in a client only covers the clients that run it.
|
|
12213
|
+
*/
|
|
12214
|
+
async function resourcesCommand(action, rawArgs) {
|
|
12215
|
+
const { client } = await requireClient(rawArgs);
|
|
12216
|
+
const projectId = await requireProject(rawArgs, client);
|
|
12217
|
+
const project = await client.data.collection("projects").findById(projectId);
|
|
12218
|
+
if (!project) fail(`Project ${displayProjectRef(rawArgs)} not found.`, void 0, "not_found");
|
|
12219
|
+
if (action !== "set") {
|
|
12220
|
+
emit(() => {
|
|
12221
|
+
console.log("");
|
|
12222
|
+
console.log(` ${chalk.bold(String(project.name ?? ""))} ${chalk.gray(`[${String(project.subdomain ?? "")}]`)}`);
|
|
12223
|
+
console.log("");
|
|
12224
|
+
keyValues([
|
|
12225
|
+
["Plan", String(project.plan ?? "legacy (no plan)")],
|
|
12226
|
+
["App CPU", dialLine(project.cpu)],
|
|
12227
|
+
["App memory", dialLine(project.memory)],
|
|
12228
|
+
["App replicas", String(project.replicaCount ?? 1)],
|
|
12229
|
+
["Database", dialLine(project.databaseMode)],
|
|
12230
|
+
["Database instances", dialLine(project.databaseInstances)],
|
|
12231
|
+
["Database CPU", dialLine(project.databaseCpu)],
|
|
12232
|
+
["Database memory", dialLine(project.databaseMemory)]
|
|
12233
|
+
]);
|
|
12234
|
+
console.log("");
|
|
12235
|
+
noteBlank();
|
|
12236
|
+
note("Empty means the plan's default. Change one with:");
|
|
12237
|
+
note(chalk.cyan(" rebase cloud resources set --cpu 500m --memory 2Gi"));
|
|
12238
|
+
console.log("");
|
|
12239
|
+
}, () => ({
|
|
12240
|
+
plan: project.plan ?? null,
|
|
12241
|
+
cpu: project.cpu ?? null,
|
|
12242
|
+
memory: project.memory ?? null,
|
|
12243
|
+
replicaCount: project.replicaCount ?? 1,
|
|
12244
|
+
databaseMode: project.databaseMode ?? null,
|
|
12245
|
+
databaseInstances: project.databaseInstances ?? null,
|
|
12246
|
+
databaseCpu: project.databaseCpu ?? null,
|
|
12247
|
+
databaseMemory: project.databaseMemory ?? null
|
|
12248
|
+
}));
|
|
12249
|
+
return;
|
|
12250
|
+
}
|
|
12251
|
+
const built = buildDialPatch(rawArgs);
|
|
12252
|
+
if (built.error) fail(built.error, void 0, "bad_request");
|
|
12253
|
+
const patch = built.patch;
|
|
12254
|
+
try {
|
|
12255
|
+
await client.data.collection("projects").update(projectId, patch);
|
|
12256
|
+
} catch (error) {
|
|
12257
|
+
reportError(error, "Could not change resources");
|
|
12258
|
+
return;
|
|
12259
|
+
}
|
|
12260
|
+
emit(() => {
|
|
12261
|
+
success("Resources updated.");
|
|
12262
|
+
noteBlank();
|
|
12263
|
+
for (const [field, value] of Object.entries(patch)) note(` ${field} → ${String(value)}`);
|
|
12264
|
+
noteBlank();
|
|
12265
|
+
note("Applied on the next deploy, or by the hourly reconcile — whichever is first.");
|
|
12266
|
+
note("A change that restarts the database waits for a maintenance window.");
|
|
12267
|
+
}, () => ({ updated: patch }));
|
|
12268
|
+
}
|
|
12269
|
+
/** A dial's value, or a marker that the plan decides it. */
|
|
12270
|
+
function dialLine(value) {
|
|
12271
|
+
if (value === null || value === void 0 || value === "") return chalk.gray("plan default");
|
|
12272
|
+
return String(value);
|
|
12273
|
+
}
|
|
12274
|
+
/**
|
|
12275
|
+
* Turn `--cpu 500m --db-instances 2` into the patch to send.
|
|
12276
|
+
*
|
|
12277
|
+
* Pure, and exported, so the flag handling is testable without a control plane —
|
|
12278
|
+
* the same shape `buildSettingsPatch` uses. Returns an error string rather than
|
|
12279
|
+
* throwing, because the caller owns how a refusal is printed in JSON mode.
|
|
12280
|
+
*/
|
|
12281
|
+
function buildDialPatch(rawArgs) {
|
|
12282
|
+
const patch = {};
|
|
12283
|
+
for (const [flag, field] of Object.entries(DIAL_FLAGS)) {
|
|
12284
|
+
const idx = rawArgs.indexOf(flag);
|
|
12285
|
+
if (idx === -1) continue;
|
|
12286
|
+
const value = rawArgs[idx + 1];
|
|
12287
|
+
if (value === void 0 || value.startsWith("--")) return {
|
|
12288
|
+
patch: {},
|
|
12289
|
+
error: `${flag} needs a value.`
|
|
12290
|
+
};
|
|
12291
|
+
if (field === "databaseInstances") {
|
|
12292
|
+
const n = Number(value);
|
|
12293
|
+
if (!Number.isInteger(n)) return {
|
|
12294
|
+
patch: {},
|
|
12295
|
+
error: `${flag} takes a whole number of instances, not "${value}".`
|
|
12296
|
+
};
|
|
12297
|
+
patch[field] = n;
|
|
12298
|
+
} else patch[field] = value;
|
|
12299
|
+
}
|
|
12300
|
+
if (Object.keys(patch).length === 0) return {
|
|
12301
|
+
patch: {},
|
|
12302
|
+
error: `Nothing to set. Pass one of: ${Object.keys(DIAL_FLAGS).join(", ")}.`
|
|
12303
|
+
};
|
|
12304
|
+
return { patch };
|
|
12305
|
+
}
|
|
11680
12306
|
//#endregion
|
|
11681
12307
|
//#region src/commands/cloud/index.ts
|
|
11682
12308
|
/**
|
|
@@ -11839,8 +12465,11 @@ async function cloudCommand(subcommand, rawArgs) {
|
|
|
11839
12465
|
case "storage":
|
|
11840
12466
|
await storageCommand(action, rawArgs);
|
|
11841
12467
|
break;
|
|
12468
|
+
case "resources":
|
|
12469
|
+
await resourcesCommand(action, rawArgs);
|
|
12470
|
+
break;
|
|
11842
12471
|
case "clusters":
|
|
11843
|
-
await clustersCommand(rawArgs);
|
|
12472
|
+
await clustersCommand(action, rawArgs);
|
|
11844
12473
|
break;
|
|
11845
12474
|
case "billing":
|
|
11846
12475
|
await billingCommand(rawArgs);
|
|
@@ -11972,6 +12601,11 @@ ${chalk.green.bold("Databases")}
|
|
|
11972
12601
|
|
|
11973
12602
|
${chalk.green.bold("Other resources")}
|
|
11974
12603
|
${chalk.blue.bold("webhooks list|create|delete")}
|
|
12604
|
+
${chalk.blue.bold("clusters")} List the clusters tenants run on
|
|
12605
|
+
${chalk.blue.bold("clusters add")} Register a cluster from a kubeconfig
|
|
12606
|
+
${chalk.blue.bold("clusters verify")} Ask a cluster whether it can host tenants
|
|
12607
|
+
${chalk.blue.bold("resources")} Show the CPU, memory and database this project is given
|
|
12608
|
+
${chalk.blue.bold("resources set")} Change them (--cpu, --memory, --db-mode, --db-instances, --db-cpu, --db-memory)
|
|
11975
12609
|
${chalk.blue.bold("storage")} List storage buckets
|
|
11976
12610
|
${chalk.blue.bold("storage create")} Provision platform-managed storage
|
|
11977
12611
|
${chalk.blue.bold("storage attach")} Attach your own S3-compatible bucket
|
|
@@ -12400,6 +13034,6 @@ function telemetryNotice() {
|
|
|
12400
13034
|
return chalk.gray(`Usage sharing: ${sharing ? "on" : "off"} — ${chalk.cyan("rebase telemetry")} to inspect or change\n`);
|
|
12401
13035
|
}
|
|
12402
13036
|
//#endregion
|
|
12403
|
-
export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, DEV_FLAGS, DEV_PORT_FILENAME, INIT_FLAGS, MANIFEST_FILENAME, ManifestError, RESET_PASSWORD_FLAGS, 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, renderPayload, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveCliVersion, resolveExampleBaseUrl, resolveLocalBin, resolvePluginCliScript, resolveResetPasswordArgs, resolveRuntimeImageTag, resolveStartPort, resolveTsx, schemaCommand, selectUserForEmail, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, writeManifest };
|
|
13037
|
+
export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, DEV_FLAGS, DEV_PORT_FILENAME, INIT_FLAGS, MANIFEST_FILENAME, ManifestError, RESET_PASSWORD_FLAGS, TEMPLATE_PLACEHOLDER_FILES, VENDOR_SIZE_WARN_BYTES, VENDOR_TARGET_CPU, VENDOR_TARGET_OS, absolutizeLocalPathArgs, 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, findLibpqUrlProblems, findProjectRoot, findUnusedServerEntry, foldStaticIntoBundle, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, getProjectPort, isIdentifierLike, isPnpmAvailable, isPortAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, positionals, printInitHelp, readEnvFile, renderPayload, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveCliVersion, resolveExampleBaseUrl, resolveLocalBin, resolvePluginCliScript, resolveResetPasswordArgs, resolveRuntimeImageTag, resolveStartPort, resolveTsx, schemaCommand, selectUserForEmail, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, vendorDependencies, writeManifest };
|
|
12404
13038
|
|
|
12405
13039
|
//# sourceMappingURL=index.es.js.map
|