@rebasepro/cli 0.17.1 → 0.17.2-canary.ga94217a
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/cloud/action-help.d.ts +26 -0
- package/dist/commands/cloud/context.d.ts +22 -0
- package/dist/commands/cloud/databases.d.ts +48 -0
- package/dist/commands/cloud/deploy.d.ts +32 -8
- package/dist/commands/cloud/errors.d.ts +80 -0
- package/dist/commands/cloud/projects.d.ts +17 -0
- package/dist/commands/cloud/resources.d.ts +66 -0
- package/dist/constraints-DKGbNfUW.js +48 -0
- package/dist/constraints-DKGbNfUW.js.map +1 -0
- package/dist/{daemon-entry-Brq-S8XX.js → daemon-entry-CmJn83zu.js} +17 -7
- package/dist/daemon-entry-CmJn83zu.js.map +1 -0
- package/dist/dev-db/constraints.d.ts +17 -1
- package/dist/index.es.js +2319 -1604
- package/dist/index.es.js.map +1 -1
- package/package.json +7 -6
- package/dist/constraints-BK1_4vci.js +0 -80
- package/dist/constraints-BK1_4vci.js.map +0 -1
- package/dist/daemon-entry-Brq-S8XX.js.map +0 -1
package/dist/index.es.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { f as __exportAll } from "./state-c0CJ6Kwb.js";
|
|
2
|
-
import { t as MANAGED_LIMITATIONS } from "./constraints-
|
|
2
|
+
import { t as MANAGED_LIMITATIONS } from "./constraints-DKGbNfUW.js";
|
|
3
3
|
import { n as ensureManagedDatabase } from "./daemon-Bdl4lrdt.js";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import arg from "arg";
|
|
@@ -493,6 +493,135 @@ function requireBackendDir(projectRoot) {
|
|
|
493
493
|
return backendDir;
|
|
494
494
|
}
|
|
495
495
|
//#endregion
|
|
496
|
+
//#region src/commands/cloud/errors.ts
|
|
497
|
+
/**
|
|
498
|
+
* The service accounts a Kubernetes 403 can name, and what each means.
|
|
499
|
+
*
|
|
500
|
+
* `system:serviceaccount:` is the platform's own identity — the control plane's
|
|
501
|
+
* pod, or a tenant's operator. A project cannot grant it anything, because the
|
|
502
|
+
* grant lives in a cluster the project does not own. `system:anonymous` is the
|
|
503
|
+
* same fact with the credential missing entirely.
|
|
504
|
+
*/
|
|
505
|
+
var PLATFORM_PRINCIPAL_RE = /system:(?:serviceaccount:|anonymous|node:)/;
|
|
506
|
+
/**
|
|
507
|
+
* A Kubernetes `Status` object embedded anywhere in a message.
|
|
508
|
+
*
|
|
509
|
+
* Scanned for rather than parsed off the front: the control plane wraps it
|
|
510
|
+
* ("Failed to create the tenant namespace: …"), the client library appends
|
|
511
|
+
* headers after it, and both halves are worth keeping out of the summary.
|
|
512
|
+
* Balanced-brace scanning rather than a regex, because `details.causes` nests
|
|
513
|
+
* and a lazy `\{.*?\}` truncates the object at the first inner brace — which
|
|
514
|
+
* parses to nothing and silently falls through to the raw message.
|
|
515
|
+
*/
|
|
516
|
+
function extractKubernetesStatus(text) {
|
|
517
|
+
for (let start = text.indexOf("{"); start !== -1; start = text.indexOf("{", start + 1)) {
|
|
518
|
+
let depth = 0;
|
|
519
|
+
let inString = false;
|
|
520
|
+
let escaped = false;
|
|
521
|
+
for (let i = start; i < text.length; i++) {
|
|
522
|
+
const ch = text[i];
|
|
523
|
+
if (escaped) {
|
|
524
|
+
escaped = false;
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (ch === "\\") {
|
|
528
|
+
escaped = true;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (ch === "\"") {
|
|
532
|
+
inString = !inString;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (inString) continue;
|
|
536
|
+
if (ch === "{") depth++;
|
|
537
|
+
else if (ch === "}") {
|
|
538
|
+
depth--;
|
|
539
|
+
if (depth !== 0) continue;
|
|
540
|
+
const candidate = text.slice(start, i + 1);
|
|
541
|
+
let parsed;
|
|
542
|
+
try {
|
|
543
|
+
parsed = JSON.parse(candidate);
|
|
544
|
+
} catch {
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
const obj = parsed;
|
|
548
|
+
if (obj?.kind === "Status" || obj?.status === "Failure" && typeof obj.code === "number") return {
|
|
549
|
+
message: typeof obj.message === "string" ? obj.message : void 0,
|
|
550
|
+
reason: typeof obj.reason === "string" ? obj.reason : void 0,
|
|
551
|
+
code: typeof obj.code === "number" ? obj.code : void 0,
|
|
552
|
+
details: obj.details ?? void 0
|
|
553
|
+
};
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Strip the transport noise a Kubernetes client appends to its own message.
|
|
561
|
+
*
|
|
562
|
+
* Only ever applied to the fallback path — when no `Status` was found, the
|
|
563
|
+
* message is all there is, and cutting it at the first header keeps the
|
|
564
|
+
* sentence while dropping the `audit-id` and the flowschema uid nobody outside
|
|
565
|
+
* the cluster can use.
|
|
566
|
+
*/
|
|
567
|
+
function trimTransportNoise(message) {
|
|
568
|
+
const cut = message.search(/\s*(?:headers:|HTTP request failed|audit-id|x-kubernetes-pf-|\{"kind":"Status")/i);
|
|
569
|
+
return (cut > 0 ? message.slice(0, cut) : message).trim().replace(/[\s:,-]+$/, "");
|
|
570
|
+
}
|
|
571
|
+
/** The longest a summary may be before it stops being one. */
|
|
572
|
+
var MAX_SUMMARY = 300;
|
|
573
|
+
/**
|
|
574
|
+
* One actionable line (plus a hint) from whatever the control plane returned.
|
|
575
|
+
*
|
|
576
|
+
* `status` is the HTTP status of the control-plane call itself, which is a
|
|
577
|
+
* different number from the `code` inside an embedded Kubernetes `Status` — the
|
|
578
|
+
* control plane routinely answers 500 while the cluster answered 403, and it is
|
|
579
|
+
* the inner one that says what happened.
|
|
580
|
+
*/
|
|
581
|
+
function summarizeError(error, context) {
|
|
582
|
+
const err = error;
|
|
583
|
+
const raw = err?.message ?? String(error);
|
|
584
|
+
const k8s = extractKubernetesStatus(raw);
|
|
585
|
+
if (k8s) {
|
|
586
|
+
const inner = k8s.message?.trim() ?? "";
|
|
587
|
+
if ((k8s.code === 403 || k8s.reason === "Forbidden") && PLATFORM_PRINCIPAL_RE.test(inner)) return {
|
|
588
|
+
message: `${context}: the platform's own cluster credentials were refused by Kubernetes.`,
|
|
589
|
+
hint: `${truncate(inner)}\n This is a platform-side permission, not something your project can grant. Nothing in your code, collections or deploy flags will change it — report it with the message above rather than retrying or changing the project.`,
|
|
590
|
+
code: "platform_permission_denied",
|
|
591
|
+
platform: true,
|
|
592
|
+
raw
|
|
593
|
+
};
|
|
594
|
+
return {
|
|
595
|
+
message: `${context}: ${truncate(inner || k8s.reason || "the cluster refused the request")}`,
|
|
596
|
+
hint: k8s.code ? `Kubernetes answered ${k8s.code}${k8s.reason ? ` (${k8s.reason})` : ""}.` : void 0,
|
|
597
|
+
code: k8s.reason ? `k8s_${k8s.reason.toLowerCase()}` : "k8s_error",
|
|
598
|
+
platform: false,
|
|
599
|
+
raw
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const trimmed = trimTransportNoise(raw);
|
|
603
|
+
return {
|
|
604
|
+
message: `${context}${err?.status ? ` (${err.status})` : ""}: ${truncate(trimmed || raw)}`,
|
|
605
|
+
code: err?.code ?? (err?.status ? `http_${err.status}` : "request_failed"),
|
|
606
|
+
platform: false,
|
|
607
|
+
raw
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function truncate(text) {
|
|
611
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
612
|
+
return flat.length > MAX_SUMMARY ? `${flat.slice(0, MAX_SUMMARY - 1)}…` : flat;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Whether the caller asked for the untouched body.
|
|
616
|
+
*
|
|
617
|
+
* `--debug` is already what `bin/rebase.js` prints after every failure as the
|
|
618
|
+
* thing to add, so the raw payload hangs off the flag people are told to reach
|
|
619
|
+
* for rather than off one invented here.
|
|
620
|
+
*/
|
|
621
|
+
function wantsRawError(argv = process.argv) {
|
|
622
|
+
return argv.includes("--debug") || process.env.REBASE_DEBUG === "1";
|
|
623
|
+
}
|
|
624
|
+
//#endregion
|
|
496
625
|
//#region src/commands/cloud/context.ts
|
|
497
626
|
/**
|
|
498
627
|
* Shared foundation for the `rebase cloud` command family.
|
|
@@ -1065,6 +1194,24 @@ function parseCloudArgs(opts) {
|
|
|
1065
1194
|
}
|
|
1066
1195
|
}
|
|
1067
1196
|
/**
|
|
1197
|
+
* `--timeout <seconds>` as milliseconds, or `fallbackMs` when it was not given.
|
|
1198
|
+
*
|
|
1199
|
+
* One function rather than one per command, because two commands take this flag
|
|
1200
|
+
* and a second copy is where the two would come to disagree about what
|
|
1201
|
+
* `--timeout 0` means.
|
|
1202
|
+
*
|
|
1203
|
+
* A value this cannot read is a refusal, not a fall back to the default. The
|
|
1204
|
+
* whole reason a caller passes a timeout is that it has a deadline of its own;
|
|
1205
|
+
* quietly substituting a different one is how a fifteen-minute wait turns up
|
|
1206
|
+
* inside a five-minute CI step, having been asked for `--timeout 30s`.
|
|
1207
|
+
*/
|
|
1208
|
+
function resolveTimeoutMs(value, opts) {
|
|
1209
|
+
if (value === void 0) return opts.fallbackMs;
|
|
1210
|
+
const seconds = Number(value);
|
|
1211
|
+
if (!Number.isFinite(seconds) || seconds <= 0) fail(`--timeout takes a number of seconds (got "${value}").`, `Run \`rebase ${opts.command} --help\`.`, "usage");
|
|
1212
|
+
return seconds * 1e3;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1068
1215
|
* Announce an outcome — "Logged in as …", "Deleted project …".
|
|
1069
1216
|
*
|
|
1070
1217
|
* On **stderr**, in both modes. It reads like a result and is not one: the
|
|
@@ -1142,19 +1289,29 @@ function keyValues(rows) {
|
|
|
1142
1289
|
/**
|
|
1143
1290
|
* Surface an SDK/HTTP error consistently. The SDK throws RebaseApiError with
|
|
1144
1291
|
* a `.status` and `.message`; anything else falls back to its string form.
|
|
1292
|
+
*
|
|
1293
|
+
* The message is summarised rather than printed — see `summarizeError`. What
|
|
1294
|
+
* arrives here is routinely a whole Kubernetes `Status` object with the request
|
|
1295
|
+
* headers appended, and the one sentence worth reading is inside it. The
|
|
1296
|
+
* untouched body is still available, behind `--debug`, on stderr where it
|
|
1297
|
+
* cannot corrupt the JSON value on stdout.
|
|
1145
1298
|
*/
|
|
1146
1299
|
function reportError(e, context) {
|
|
1147
1300
|
const err = e;
|
|
1301
|
+
const summary = summarizeError(e, context);
|
|
1302
|
+
if (wantsRawError()) process.stderr.write(`\n${summary.raw}\n\n`);
|
|
1148
1303
|
if (JSON_MODE) {
|
|
1149
1304
|
printJson({ error: {
|
|
1150
|
-
message:
|
|
1151
|
-
code:
|
|
1305
|
+
message: stripAnsi(summary.message),
|
|
1306
|
+
code: summary.code,
|
|
1152
1307
|
status: err?.status ?? null,
|
|
1308
|
+
hint: summary.hint ? stripAnsi(summary.hint) : void 0,
|
|
1309
|
+
platform: summary.platform,
|
|
1153
1310
|
context
|
|
1154
1311
|
} });
|
|
1155
1312
|
process.exit(1);
|
|
1156
1313
|
}
|
|
1157
|
-
fail(
|
|
1314
|
+
fail(summary.message, summary.hint, summary.code);
|
|
1158
1315
|
}
|
|
1159
1316
|
/**
|
|
1160
1317
|
* Open a URL in the user's default browser (best effort). Always announces the
|
|
@@ -2972,8 +3129,8 @@ async function runDriverDbCommand(rawArgs, options = {}) {
|
|
|
2972
3129
|
});
|
|
2973
3130
|
}
|
|
2974
3131
|
async function dbCommand(subcommand, rawArgs) {
|
|
2975
|
-
if (!subcommand || subcommand === "--help") {
|
|
2976
|
-
printDbHelp$1();
|
|
3132
|
+
if (!subcommand || subcommand === "--help" || rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
3133
|
+
printDbHelp$1(subcommand === "--help" ? void 0 : subcommand);
|
|
2977
3134
|
return;
|
|
2978
3135
|
}
|
|
2979
3136
|
const projectRoot = requireProjectRoot();
|
|
@@ -3135,7 +3292,76 @@ async function pullIntoLocal(projectRoot, rawArgs) {
|
|
|
3135
3292
|
fs.rmSync(dumpFile, { force: true });
|
|
3136
3293
|
}
|
|
3137
3294
|
}
|
|
3138
|
-
|
|
3295
|
+
/**
|
|
3296
|
+
* What each `rebase db <action>` does and takes.
|
|
3297
|
+
*
|
|
3298
|
+
* Kept here rather than delegated to the driver because the driver is reached
|
|
3299
|
+
* by *running* it — which is exactly what `--help` must not do. The page is
|
|
3300
|
+
* deliberately short: the authority on a flag is the command's own spec, and
|
|
3301
|
+
* `check-doc-commands.mjs` holds every command written in this repository's
|
|
3302
|
+
* markdown to it. What a reader needs from here is which subcommands exist and
|
|
3303
|
+
* what the destructive ones want before they will run.
|
|
3304
|
+
*/
|
|
3305
|
+
var DB_ACTION_HELP = {
|
|
3306
|
+
push: {
|
|
3307
|
+
usage: "rebase db push [--collections <dir>] [--allow-destructive] [--yes]",
|
|
3308
|
+
summary: "Apply the schema straight to the database. Development only — it does not write a migration.",
|
|
3309
|
+
notes: ["A change that would drop data needs --allow-destructive."]
|
|
3310
|
+
},
|
|
3311
|
+
generate: {
|
|
3312
|
+
usage: "rebase db generate [--collections <dir>]",
|
|
3313
|
+
summary: "Generate the Drizzle schema, the Postgres DDL and a SQL migration file from the collections."
|
|
3314
|
+
},
|
|
3315
|
+
migrate: {
|
|
3316
|
+
usage: "rebase db migrate",
|
|
3317
|
+
summary: "Run the pending migration files against the database."
|
|
3318
|
+
},
|
|
3319
|
+
branch: {
|
|
3320
|
+
usage: "rebase db branch <create|list|delete|info> [name]",
|
|
3321
|
+
summary: "Database branching."
|
|
3322
|
+
},
|
|
3323
|
+
backup: {
|
|
3324
|
+
usage: "rebase db backup [--out <path|s3://…>]",
|
|
3325
|
+
summary: "Create a pg_dump backup. --out is resolved against the directory you are standing in."
|
|
3326
|
+
},
|
|
3327
|
+
backups: {
|
|
3328
|
+
usage: "rebase db backups",
|
|
3329
|
+
summary: "List stored backups."
|
|
3330
|
+
},
|
|
3331
|
+
restore: {
|
|
3332
|
+
usage: "rebase db restore <dump> [--target-db <name>] [--create-db] --yes",
|
|
3333
|
+
summary: "Restore a backup with pg_restore.",
|
|
3334
|
+
notes: ["Destructive, and refuses to run without --yes."]
|
|
3335
|
+
},
|
|
3336
|
+
pull: {
|
|
3337
|
+
usage: "rebase db pull --from <url> [--schema <name>] [--anonymize] [--yes]",
|
|
3338
|
+
summary: "Copy another database into local development. One-directional by design — it can never push."
|
|
3339
|
+
},
|
|
3340
|
+
stop: {
|
|
3341
|
+
usage: "rebase db stop",
|
|
3342
|
+
summary: "Stop the managed development database. Data is kept."
|
|
3343
|
+
},
|
|
3344
|
+
reset: {
|
|
3345
|
+
usage: "rebase db reset [--yes]",
|
|
3346
|
+
summary: "Delete the managed development database and start over.",
|
|
3347
|
+
notes: ["Destructive, and the data exists only on this machine."]
|
|
3348
|
+
}
|
|
3349
|
+
};
|
|
3350
|
+
function printDbHelp$1(action) {
|
|
3351
|
+
const entry = action ? DB_ACTION_HELP[action] : void 0;
|
|
3352
|
+
if (entry) {
|
|
3353
|
+
console.log(`
|
|
3354
|
+
${chalk.bold(`rebase db ${action}`)}
|
|
3355
|
+
|
|
3356
|
+
${entry.summary}
|
|
3357
|
+
|
|
3358
|
+
${chalk.green.bold("Usage")}
|
|
3359
|
+
${chalk.blue(entry.usage)}
|
|
3360
|
+
${entry.notes?.length ? `\n${chalk.green.bold("Notes")}\n${entry.notes.map((n) => ` ${chalk.gray(`• ${n}`)}`).join("\n")}\n` : ""}
|
|
3361
|
+
${chalk.gray("Run `rebase db --help` for every subcommand.")}
|
|
3362
|
+
`);
|
|
3363
|
+
return;
|
|
3364
|
+
}
|
|
3139
3365
|
console.log(`
|
|
3140
3366
|
${chalk.bold("rebase db")} — Database management commands
|
|
3141
3367
|
|
|
@@ -9122,6 +9348,39 @@ function describeDatabaseState(db) {
|
|
|
9122
9348
|
if (connection === "connected" || connection === "failed") return `${type} (${colorStatus(connection)})`;
|
|
9123
9349
|
return `${type} ${chalk.gray("· not tested (`rebase cloud db test`)")}`;
|
|
9124
9350
|
}
|
|
9351
|
+
/** Deployment states that mean the platform is still doing something. */
|
|
9352
|
+
var IN_FLIGHT = /* @__PURE__ */ new Set([
|
|
9353
|
+
"deploying",
|
|
9354
|
+
"building",
|
|
9355
|
+
"pending",
|
|
9356
|
+
"queued"
|
|
9357
|
+
]);
|
|
9358
|
+
function resolveBlockedState(input) {
|
|
9359
|
+
if (!input.database) return {
|
|
9360
|
+
blockedOn: "no_database",
|
|
9361
|
+
nextAction: "rebase cloud db create --type managed"
|
|
9362
|
+
};
|
|
9363
|
+
if (input.lastDeploy && IN_FLIGHT.has(String(input.lastDeploy.status))) return {
|
|
9364
|
+
blockedOn: null,
|
|
9365
|
+
nextAction: null
|
|
9366
|
+
};
|
|
9367
|
+
if (!input.lastDeploy) return {
|
|
9368
|
+
blockedOn: "never_deployed",
|
|
9369
|
+
nextAction: "rebase cloud deploy"
|
|
9370
|
+
};
|
|
9371
|
+
if (input.lastDeploy.status && input.lastDeploy.status !== "success") return {
|
|
9372
|
+
blockedOn: "last_deploy_failed",
|
|
9373
|
+
nextAction: "rebase cloud logs"
|
|
9374
|
+
};
|
|
9375
|
+
if (input.database.connectionStatus === "failed") return {
|
|
9376
|
+
blockedOn: "database_unreachable",
|
|
9377
|
+
nextAction: "rebase cloud db test"
|
|
9378
|
+
};
|
|
9379
|
+
return {
|
|
9380
|
+
blockedOn: null,
|
|
9381
|
+
nextAction: null
|
|
9382
|
+
};
|
|
9383
|
+
}
|
|
9125
9384
|
/**
|
|
9126
9385
|
* One line describing what engine is serving this project.
|
|
9127
9386
|
*
|
|
@@ -9161,6 +9420,11 @@ async function statusCommand(rawArgs) {
|
|
|
9161
9420
|
]);
|
|
9162
9421
|
const storageLine = describeStorageState(storage);
|
|
9163
9422
|
const databaseLine = describeDatabaseState(db);
|
|
9423
|
+
const blocked = resolveBlockedState({
|
|
9424
|
+
projectStatus: project.status,
|
|
9425
|
+
database: db,
|
|
9426
|
+
lastDeploy: deploy
|
|
9427
|
+
});
|
|
9164
9428
|
emit(() => {
|
|
9165
9429
|
console.log("");
|
|
9166
9430
|
console.log(` ${chalk.bold(project.name ?? project.subdomain ?? "")} ${chalk.gray(`[${project.subdomain ?? displayProjectRef(rawArgs)}]`)} ${colorStatus(project.status)}`);
|
|
@@ -9173,12 +9437,19 @@ async function statusCommand(rawArgs) {
|
|
|
9173
9437
|
["Database", databaseLine],
|
|
9174
9438
|
["Storage", storageLine]
|
|
9175
9439
|
]);
|
|
9440
|
+
if (blocked.blockedOn) {
|
|
9441
|
+
console.log("");
|
|
9442
|
+
console.log(` ${chalk.yellow("Waiting on you")} ${chalk.gray(`— ${blocked.blockedOn}`)}`);
|
|
9443
|
+
console.log(` ${chalk.bold(blocked.nextAction ?? "")}`);
|
|
9444
|
+
}
|
|
9176
9445
|
console.log("");
|
|
9177
9446
|
}, {
|
|
9178
9447
|
projectId: String(project.id),
|
|
9179
9448
|
name: project.name ?? null,
|
|
9180
9449
|
subdomain: project.subdomain ?? null,
|
|
9181
9450
|
status: project.status ?? null,
|
|
9451
|
+
blockedOn: blocked.blockedOn,
|
|
9452
|
+
nextAction: blocked.nextAction,
|
|
9182
9453
|
url: projectHost(project, baseDomain) ?? null,
|
|
9183
9454
|
branch: project.gitBranch ?? null,
|
|
9184
9455
|
lastDeploy: deploy ? {
|
|
@@ -9589,16 +9860,48 @@ async function clustersCommand(action, rawArgs) {
|
|
|
9589
9860
|
* answered by a customer's first deploy failing halfway through provisioning,
|
|
9590
9861
|
* with an error they cannot act on and half a tenant already created.
|
|
9591
9862
|
*/
|
|
9863
|
+
/**
|
|
9864
|
+
* Which cluster `clusters verify` was asked about, and whether `--baseline`
|
|
9865
|
+
* was given.
|
|
9866
|
+
*
|
|
9867
|
+
* Resolved against a real spec, not scanned out of `rawArgs` by hand — and this
|
|
9868
|
+
* is not a tidy-up. `rawArgs` is the whole `process.argv`, so the old scan
|
|
9869
|
+
* ("the first token that is not `--…` and is neither `clusters` nor `verify`")
|
|
9870
|
+
* matched `argv[0]`, the **node binary path**. Every `rebase cloud clusters
|
|
9871
|
+
* verify <id>` therefore asked the control plane about a cluster called
|
|
9872
|
+
* `/usr/local/bin/node`, and came back 404.
|
|
9873
|
+
*
|
|
9874
|
+
* So the one diagnostic that reports `permissions.allowed` /
|
|
9875
|
+
* `permissions.denied` was unreachable, and its 404 read as "this command is
|
|
9876
|
+
* not deployed yet" rather than "the id never left this machine intact". It is
|
|
9877
|
+
* the command that names a missing `cronjobs.batch` grant in a single call
|
|
9878
|
+
* instead of a twenty-minute A/B against a live project.
|
|
9879
|
+
*
|
|
9880
|
+
* Same failure as `cloud deploy` reading `_[0]` as `"cloud"`, and the same fix:
|
|
9881
|
+
* one parser, exported so its test drives the real thing rather than a copy.
|
|
9882
|
+
*/
|
|
9883
|
+
function resolveClusterVerifyArgs(rawArgs) {
|
|
9884
|
+
const { flags, positionals } = parseCloudArgs({
|
|
9885
|
+
spec: { "--baseline": Boolean },
|
|
9886
|
+
rawArgs,
|
|
9887
|
+
commandWords: 2,
|
|
9888
|
+
command: "cloud clusters verify",
|
|
9889
|
+
maxPositionals: 2
|
|
9890
|
+
});
|
|
9891
|
+
return {
|
|
9892
|
+
id: positionals[1],
|
|
9893
|
+
baseline: flags["--baseline"] === true
|
|
9894
|
+
};
|
|
9895
|
+
}
|
|
9592
9896
|
async function clustersVerifyCommand(rawArgs) {
|
|
9593
|
-
const {
|
|
9594
|
-
const id = rawArgs.find((a) => !a.startsWith("--") && a !== "clusters" && a !== "verify");
|
|
9897
|
+
const { id, baseline: withBaseline } = resolveClusterVerifyArgs(rawArgs);
|
|
9595
9898
|
if (!id) fail("Usage: rebase cloud clusters verify <cluster-id> [--baseline]", void 0, "bad_request");
|
|
9596
|
-
const
|
|
9899
|
+
const { client } = await requireClient(rawArgs);
|
|
9597
9900
|
let report;
|
|
9598
9901
|
try {
|
|
9599
9902
|
report = await client.functions.invoke("cluster-baseline", void 0, {
|
|
9600
9903
|
method: "GET",
|
|
9601
|
-
path: `verify/${id}${withBaseline ? "?baseline=1" : ""}`
|
|
9904
|
+
path: `verify/${encodeURIComponent(id)}${withBaseline ? "?baseline=1" : ""}`
|
|
9602
9905
|
});
|
|
9603
9906
|
} catch (error) {
|
|
9604
9907
|
reportError(error, "Could not verify the cluster");
|
|
@@ -10012,1834 +10315,2039 @@ function buildDialPatch(rawArgs, opts) {
|
|
|
10012
10315
|
return { patch };
|
|
10013
10316
|
}
|
|
10014
10317
|
//#endregion
|
|
10015
|
-
//#region src/commands/cloud/
|
|
10318
|
+
//#region src/commands/cloud/databases.ts
|
|
10016
10319
|
/**
|
|
10017
|
-
* `rebase cloud
|
|
10320
|
+
* `rebase cloud db` — database + backup management for a project.
|
|
10321
|
+
*
|
|
10322
|
+
* db list List databases attached to the project
|
|
10323
|
+
* db create Attach a managed or bring-your-own database
|
|
10324
|
+
* db test Test connectivity to the project's database
|
|
10325
|
+
* db backup list|create|restore
|
|
10018
10326
|
*/
|
|
10019
|
-
async function
|
|
10020
|
-
|
|
10021
|
-
|
|
10327
|
+
async function dbCommand$1(subcommand, rawArgs) {
|
|
10328
|
+
switch (subcommand) {
|
|
10329
|
+
case "list":
|
|
10330
|
+
case void 0:
|
|
10331
|
+
await listDatabases(rawArgs);
|
|
10332
|
+
break;
|
|
10333
|
+
case "create":
|
|
10334
|
+
await createDatabase(rawArgs);
|
|
10335
|
+
break;
|
|
10336
|
+
case "info":
|
|
10337
|
+
await dbInfo(rawArgs);
|
|
10338
|
+
break;
|
|
10339
|
+
case "test":
|
|
10340
|
+
await testDatabase(rawArgs);
|
|
10341
|
+
break;
|
|
10342
|
+
case "backup":
|
|
10343
|
+
await backupCommand(rawArgs);
|
|
10344
|
+
break;
|
|
10345
|
+
case "pitr":
|
|
10346
|
+
await pitrCommand(rawArgs);
|
|
10347
|
+
break;
|
|
10348
|
+
case "--help":
|
|
10349
|
+
printDbHelp();
|
|
10350
|
+
break;
|
|
10351
|
+
default: fail(`Unknown db command: ${subcommand}`, "Run `rebase cloud db --help`.", "unknown_command");
|
|
10352
|
+
}
|
|
10353
|
+
}
|
|
10354
|
+
async function listDatabases(rawArgs) {
|
|
10355
|
+
const { client } = await requireClient(rawArgs);
|
|
10356
|
+
const projectId = await requireProject(rawArgs, client);
|
|
10357
|
+
const projectRef = displayProjectRef(rawArgs);
|
|
10022
10358
|
try {
|
|
10023
|
-
const
|
|
10024
|
-
where:
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
}).then((res) => res.data), fetchTenantBaseDomain(client, url)]);
|
|
10028
|
-
const linkedId = readLink()?.projectId;
|
|
10359
|
+
const dbs = (await client.data.collection("databases").find({
|
|
10360
|
+
where: { project: ["==", projectId] },
|
|
10361
|
+
limit: 50
|
|
10362
|
+
})).data;
|
|
10029
10363
|
emit(() => {
|
|
10030
10364
|
console.log("");
|
|
10031
|
-
console.log(chalk.bold(
|
|
10365
|
+
console.log(chalk.bold(` 🗄 Databases — project ${projectRef}`));
|
|
10032
10366
|
console.log("");
|
|
10033
|
-
if (
|
|
10034
|
-
console.log(chalk.gray(" No
|
|
10367
|
+
if (dbs.length === 0) {
|
|
10368
|
+
console.log(chalk.gray(" No database attached. Add one with `rebase cloud db create`."));
|
|
10035
10369
|
console.log("");
|
|
10036
10370
|
return;
|
|
10037
10371
|
}
|
|
10038
|
-
for (const
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
console.log(` ${chalk.gray(projectHost(p, baseDomain) ?? "—")}${p.provider ? chalk.gray(` · ${p.provider}`) : ""}`);
|
|
10372
|
+
for (const d of dbs) {
|
|
10373
|
+
console.log(` ${chalk.bold(d.type ?? "unknown")} ${chalk.gray(`[${d.id}]`)} ${colorStatus(d.connectionStatus)}`);
|
|
10374
|
+
keyValues([["SSH tunnel", d.useSshTunnel ? "yes" : void 0], ["PITR", d.pitrEnabled ? "enabled" : void 0]]);
|
|
10042
10375
|
}
|
|
10043
10376
|
console.log("");
|
|
10044
10377
|
}, {
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
id: String(
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
provider: p.provider ?? null,
|
|
10053
|
-
linked: String(p.id) === linkedId
|
|
10378
|
+
projectId,
|
|
10379
|
+
databases: dbs.map((d) => ({
|
|
10380
|
+
id: String(d.id),
|
|
10381
|
+
type: d.type ?? null,
|
|
10382
|
+
connectionStatus: d.connectionStatus ?? null,
|
|
10383
|
+
useSshTunnel: Boolean(d.useSshTunnel),
|
|
10384
|
+
pitrEnabled: Boolean(d.pitrEnabled)
|
|
10054
10385
|
}))
|
|
10055
10386
|
});
|
|
10056
10387
|
} catch (e) {
|
|
10057
|
-
reportError(e, "Failed to list
|
|
10388
|
+
reportError(e, "Failed to list databases");
|
|
10058
10389
|
}
|
|
10059
10390
|
}
|
|
10060
10391
|
/**
|
|
10061
|
-
*
|
|
10392
|
+
* The database already attached to a project, if any.
|
|
10062
10393
|
*
|
|
10063
|
-
*
|
|
10064
|
-
*
|
|
10065
|
-
*
|
|
10066
|
-
* or refuse. What a project reserves is a set of dials now, and a project that
|
|
10067
|
-
* sets none takes the platform default.
|
|
10394
|
+
* `limit: 1` deliberately mirrors what the control plane's own `db-test`,
|
|
10395
|
+
* `db-info` and `backup` do — asking the same question the same way is the
|
|
10396
|
+
* point, since the answer decides which row a deploy will actually use.
|
|
10068
10397
|
*/
|
|
10069
|
-
function
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
}
|
|
10398
|
+
async function firstAttachedDatabase(client, projectId) {
|
|
10399
|
+
return (await client.data.collection("databases").find({
|
|
10400
|
+
where: { project: ["==", projectId] },
|
|
10401
|
+
limit: 1
|
|
10402
|
+
})).data[0];
|
|
10075
10403
|
}
|
|
10076
10404
|
/**
|
|
10077
|
-
*
|
|
10078
|
-
*
|
|
10079
|
-
* `provider`/`region` are a *request*: no code downstream reads them to pick a
|
|
10080
|
-
* deploy target — that comes from the project's cluster record or the ambient
|
|
10081
|
-
* in-cluster context (saas/backend/src/k8s/resolve.ts). So a wrong value here is
|
|
10082
|
-
* never contradicted by a failure; it just sits in the record. The CLI used to
|
|
10083
|
-
* default to `hetzner`/`nbg1` unconditionally, which is how projects running on
|
|
10084
|
-
* our GKE cluster came to describe themselves as Hetzner in the console — and
|
|
10085
|
-
* `provider` also decides which substrate's rules a project's dials are clamped
|
|
10086
|
-
* to — Autopilot's 250m floor and 1:1-6.5:1 band do not apply on Hetzner or EKS
|
|
10087
|
-
* — so a wrong value here resizes pods, it is not a cosmetic slip.
|
|
10088
|
-
*
|
|
10089
|
-
* The control plane already publishes the infrastructure that actually exists,
|
|
10090
|
-
* and the console's create wizard reads it. Ask the same question here.
|
|
10091
|
-
*
|
|
10092
|
-
* Exported for tests: the decision is pure, so it can be pinned without a
|
|
10093
|
-
* control plane. The fetching and the exit live in `resolveRequestedTarget`.
|
|
10405
|
+
* Attach a database row to a project.
|
|
10094
10406
|
*
|
|
10095
|
-
*
|
|
10096
|
-
*
|
|
10097
|
-
*
|
|
10098
|
-
*
|
|
10099
|
-
* request. That is different from an empty list, which is a control plane
|
|
10100
|
-
* stating it has no infrastructure at all.
|
|
10101
|
-
* @returns the target to record, or `null` when the control plane answered that
|
|
10102
|
-
* there is none.
|
|
10407
|
+
* Extracted so `rebase cloud projects create` can do it in the same breath as
|
|
10408
|
+
* creating the project — see `--db` there. Two call sites, one insert, so the
|
|
10409
|
+
* shape of the row cannot drift between "attached at creation" and "attached
|
|
10410
|
+
* afterwards".
|
|
10103
10411
|
*/
|
|
10104
|
-
function
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
region: void 0
|
|
10112
|
-
};
|
|
10113
|
-
if (targets.length === 0) return null;
|
|
10114
|
-
const [target] = targets;
|
|
10115
|
-
return {
|
|
10116
|
-
provider: target.provider,
|
|
10117
|
-
region: target.region?.trim() || void 0
|
|
10118
|
-
};
|
|
10119
|
-
}
|
|
10120
|
-
async function resolveRequestedTarget(client, url, requested) {
|
|
10121
|
-
const chosen = chooseRequestedTarget(requested, await fetchDeployTargets(client, url));
|
|
10122
|
-
if (!chosen) fail("This control plane has no deploy targets configured.", `Register a cluster, or pass ${chalk.bold("--provider")} and ${chalk.bold("--region")} to record one anyway.`, "no_deploy_targets");
|
|
10123
|
-
return chosen;
|
|
10412
|
+
async function attachDatabaseRow(client, input) {
|
|
10413
|
+
return await client.data.collection("databases").create({
|
|
10414
|
+
project: input.projectId,
|
|
10415
|
+
type: input.type,
|
|
10416
|
+
connectionString: input.type === "byodb" ? input.connectionString : void 0,
|
|
10417
|
+
connectionStatus: "untested"
|
|
10418
|
+
});
|
|
10124
10419
|
}
|
|
10125
|
-
/**
|
|
10126
|
-
var
|
|
10127
|
-
"--
|
|
10128
|
-
"--
|
|
10129
|
-
"--
|
|
10130
|
-
"--
|
|
10131
|
-
"--
|
|
10132
|
-
"
|
|
10133
|
-
"--org": String,
|
|
10134
|
-
"--link": Boolean,
|
|
10135
|
-
"-n": "--name",
|
|
10136
|
-
"--cpu": String,
|
|
10137
|
-
"--memory": String,
|
|
10138
|
-
"--replicas": String,
|
|
10139
|
-
"--spot": String,
|
|
10140
|
-
"--scale-to-zero": String,
|
|
10141
|
-
"--db-mode": String,
|
|
10142
|
-
"--db-instances": String,
|
|
10143
|
-
"--db-cpu": String,
|
|
10144
|
-
"--db-memory": String,
|
|
10145
|
-
"--storage": String
|
|
10420
|
+
/** What `rebase cloud db create` parses. Exported so its help page cannot drift. */
|
|
10421
|
+
var CREATE_DATABASE_FLAGS = {
|
|
10422
|
+
"--type": String,
|
|
10423
|
+
"--connection-string": String,
|
|
10424
|
+
"--wait": Boolean,
|
|
10425
|
+
"--timeout": String,
|
|
10426
|
+
"--project": String,
|
|
10427
|
+
"-p": "--project"
|
|
10146
10428
|
};
|
|
10147
|
-
async function
|
|
10148
|
-
const
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
commandWords: 3,
|
|
10152
|
-
command: "cloud projects create",
|
|
10153
|
-
maxPositionals: 0
|
|
10154
|
-
});
|
|
10155
|
-
const { client, url } = await requireClient(rawArgs);
|
|
10156
|
-
const org = args["--org"] || getContextOrg(url);
|
|
10157
|
-
if (!org) fail("No organization selected.", `Pass ${chalk.bold("--org <id>")} or run ${chalk.bold("rebase cloud use")}.`, "no_org");
|
|
10158
|
-
const prompts = [];
|
|
10159
|
-
if (!args["--name"]) prompts.push({
|
|
10160
|
-
type: "input",
|
|
10161
|
-
name: "name",
|
|
10162
|
-
message: "Project name:"
|
|
10163
|
-
});
|
|
10164
|
-
if (!args["--subdomain"]) prompts.push({
|
|
10165
|
-
type: "input",
|
|
10166
|
-
name: "subdomain",
|
|
10167
|
-
message: "Subdomain:"
|
|
10429
|
+
async function createDatabase(rawArgs) {
|
|
10430
|
+
const args = arg(CREATE_DATABASE_FLAGS, {
|
|
10431
|
+
argv: rawArgs.slice(4),
|
|
10432
|
+
permissive: true
|
|
10168
10433
|
});
|
|
10169
|
-
const
|
|
10170
|
-
const
|
|
10171
|
-
const
|
|
10172
|
-
const
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10434
|
+
const { client } = await requireClient(rawArgs);
|
|
10435
|
+
const projectId = await requireProject(rawArgs, client);
|
|
10436
|
+
const projectRef = displayProjectRef(rawArgs);
|
|
10437
|
+
const existing = await firstAttachedDatabase(client, projectId);
|
|
10438
|
+
if (existing) fail(`Project ${projectRef} already has a ${existing.type ?? "database"} attached (${existing.id}).`, "A project has exactly one database. Remove that one first, or run `rebase cloud db info` to see what it points at.", "database_exists");
|
|
10439
|
+
let type = args["--type"];
|
|
10440
|
+
if (!type) {
|
|
10441
|
+
requireInteractive("a database type", "--type <managed|byodb>");
|
|
10442
|
+
const { picked } = await inquirer.prompt([{
|
|
10443
|
+
type: "select",
|
|
10444
|
+
name: "picked",
|
|
10445
|
+
message: "Database type:",
|
|
10446
|
+
choices: [{
|
|
10447
|
+
name: "SaaS Managed (provisioned for you)",
|
|
10448
|
+
value: "managed"
|
|
10449
|
+
}, {
|
|
10450
|
+
name: "Bring Your Own DB (external PostgreSQL)",
|
|
10451
|
+
value: "byodb"
|
|
10452
|
+
}]
|
|
10453
|
+
}]);
|
|
10454
|
+
type = picked;
|
|
10455
|
+
}
|
|
10456
|
+
let connectionString = args["--connection-string"];
|
|
10457
|
+
if (type === "byodb" && !connectionString) {
|
|
10458
|
+
requireInteractive("a connection string", "--connection-string <url>");
|
|
10459
|
+
const { cs } = await inquirer.prompt([{
|
|
10460
|
+
type: "input",
|
|
10461
|
+
name: "cs",
|
|
10462
|
+
message: "PostgreSQL connection string:"
|
|
10463
|
+
}]);
|
|
10464
|
+
connectionString = cs?.trim();
|
|
10465
|
+
if (!connectionString) fail("A connection string is required for bring-your-own databases.", "Pass `--connection-string <url>`.", "input_required");
|
|
10466
|
+
}
|
|
10467
|
+
let created;
|
|
10185
10468
|
try {
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
subdomain,
|
|
10191
|
-
gitRepoUrl,
|
|
10192
|
-
gitBranch,
|
|
10193
|
-
provider,
|
|
10194
|
-
region,
|
|
10195
|
-
...dials.patch,
|
|
10196
|
-
organization: org,
|
|
10197
|
-
createdById: user.uid,
|
|
10198
|
-
status: "provisioning"
|
|
10199
|
-
});
|
|
10200
|
-
const host = projectHost(created, await fetchTenantBaseDomain(client, url));
|
|
10201
|
-
const linked = Boolean(args["--link"]);
|
|
10202
|
-
if (linked) writeLink({
|
|
10203
|
-
url,
|
|
10204
|
-
projectId: String(created.id),
|
|
10205
|
-
slug: created.subdomain,
|
|
10206
|
-
projectName: name,
|
|
10207
|
-
orgId: String(org)
|
|
10208
|
-
});
|
|
10209
|
-
success(`Created project ${chalk.bold(name)}`);
|
|
10210
|
-
emit(() => {
|
|
10211
|
-
keyValues([
|
|
10212
|
-
["Slug", String(created.subdomain ?? "")],
|
|
10213
|
-
["URL", host],
|
|
10214
|
-
["Provider", provider],
|
|
10215
|
-
["Branch", gitBranch]
|
|
10216
|
-
]);
|
|
10217
|
-
if (linked) note(chalk.gray("Linked this directory to the new project."));
|
|
10218
|
-
noteBlank();
|
|
10219
|
-
note(chalk.gray(`Deploy it with: ${chalk.bold(`rebase cloud deploy --project ${created.subdomain ?? created.id}`)}`));
|
|
10220
|
-
noteBlank();
|
|
10221
|
-
}, {
|
|
10222
|
-
success: true,
|
|
10223
|
-
id: String(created.id),
|
|
10224
|
-
name,
|
|
10225
|
-
slug: created.subdomain ?? null,
|
|
10226
|
-
host: host ?? null,
|
|
10227
|
-
provider,
|
|
10228
|
-
region,
|
|
10229
|
-
dials: dials.patch,
|
|
10230
|
-
branch: gitBranch,
|
|
10231
|
-
org: String(org),
|
|
10232
|
-
linked
|
|
10469
|
+
created = await attachDatabaseRow(client, {
|
|
10470
|
+
projectId,
|
|
10471
|
+
type,
|
|
10472
|
+
connectionString
|
|
10233
10473
|
});
|
|
10234
10474
|
} catch (e) {
|
|
10235
|
-
reportError(e, "Failed to
|
|
10475
|
+
reportError(e, "Failed to attach database");
|
|
10236
10476
|
}
|
|
10477
|
+
const waited = args["--wait"] === true ? await waitForDatabase(client, {
|
|
10478
|
+
projectId,
|
|
10479
|
+
type,
|
|
10480
|
+
timeoutMs: resolveTimeoutMs(args["--timeout"], {
|
|
10481
|
+
fallbackMs: DEFAULT_WAIT_MS,
|
|
10482
|
+
command: "cloud db create"
|
|
10483
|
+
})
|
|
10484
|
+
}) : void 0;
|
|
10485
|
+
success(`Attached ${type} database to project ${projectRef}`);
|
|
10486
|
+
emit(() => {
|
|
10487
|
+
keyValues([["ID", String(created.id)]]);
|
|
10488
|
+
if (waited?.note) note(chalk.gray(waited.note));
|
|
10489
|
+
else if (type === "byodb") note(chalk.gray("Verify it with `rebase cloud db test`."));
|
|
10490
|
+
else note(chalk.gray("It is created at your first deploy — run `rebase cloud deploy` next."));
|
|
10491
|
+
noteBlank();
|
|
10492
|
+
}, {
|
|
10493
|
+
success: true,
|
|
10494
|
+
id: String(created.id),
|
|
10495
|
+
projectId,
|
|
10496
|
+
type,
|
|
10497
|
+
connectionStatus: waited?.connectionStatus ?? "untested",
|
|
10498
|
+
waited: waited ? waited.waited : false,
|
|
10499
|
+
materializedAt: type === "managed" ? "first_deploy" : "now"
|
|
10500
|
+
});
|
|
10237
10501
|
}
|
|
10502
|
+
/** How long `db create --wait` waits by default, and `--timeout` overrides. */
|
|
10503
|
+
var DEFAULT_WAIT_MS = 300 * 1e3;
|
|
10504
|
+
var WAIT_POLL_MS = 3e3;
|
|
10238
10505
|
/**
|
|
10239
|
-
*
|
|
10240
|
-
*
|
|
10241
|
-
* The id is optional — omitted, it falls back to `--project` or the link file —
|
|
10242
|
-
* and the dispatcher used to read it off `positionals()`, which skips only
|
|
10243
|
-
* LEADING `-` tokens and declares only the global cloud flags. So an undeclared
|
|
10244
|
-
* flag written after the action became the id: `rebase cloud projects delete
|
|
10245
|
-
* --force` looked up a project named "--force" and reported it missing, rather
|
|
10246
|
-
* than saying there is no such flag. Benign next to the deletes and writes the
|
|
10247
|
-
* rest of this family aimed at the wrong resource, but the same mistake, and
|
|
10248
|
-
* `positionals()` has no spec with which to do better — the handler's own
|
|
10249
|
-
* module does.
|
|
10506
|
+
* Wait for an attached database to become usable, where "usable" means
|
|
10507
|
+
* something.
|
|
10250
10508
|
*
|
|
10251
|
-
*
|
|
10509
|
+
* Returns `waited: false` for the managed case, and says why — the caller then
|
|
10510
|
+
* knows the state it is looking at is final rather than early.
|
|
10252
10511
|
*/
|
|
10253
|
-
function
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10512
|
+
async function waitForDatabase(client, opts) {
|
|
10513
|
+
if (opts.type !== "byodb") return {
|
|
10514
|
+
waited: false,
|
|
10515
|
+
connectionStatus: "untested",
|
|
10516
|
+
note: "Nothing to wait for: a managed database is created at the project's first deploy. Run `rebase cloud deploy` next; `rebase cloud db test` only answers after that."
|
|
10517
|
+
};
|
|
10518
|
+
const started = Date.now();
|
|
10519
|
+
for (;;) {
|
|
10520
|
+
try {
|
|
10521
|
+
if ((await client.functions.invoke("db-test", { projectId: opts.projectId })).success) return {
|
|
10522
|
+
waited: true,
|
|
10523
|
+
connectionStatus: "connected"
|
|
10524
|
+
};
|
|
10525
|
+
} catch {}
|
|
10526
|
+
if (Date.now() - started > opts.timeoutMs) fail(`The database did not become reachable within ${Math.round(opts.timeoutMs / 1e3)}s.`, "Run `rebase cloud db test` for the connection log.", "timeout");
|
|
10527
|
+
await new Promise((resolve) => setTimeout(resolve, opts.pollMs ?? WAIT_POLL_MS));
|
|
10528
|
+
}
|
|
10529
|
+
}
|
|
10530
|
+
async function testDatabase(rawArgs) {
|
|
10531
|
+
const { client } = await requireClient(rawArgs);
|
|
10532
|
+
const projectId = await requireProject(rawArgs, client);
|
|
10533
|
+
const projectRef = displayProjectRef(rawArgs);
|
|
10534
|
+
noteBlank();
|
|
10535
|
+
note(`Testing database connectivity for project ${chalk.bold(projectRef)}...`);
|
|
10536
|
+
try {
|
|
10537
|
+
const res = await client.functions.invoke("db-test", { projectId });
|
|
10538
|
+
if (res.logs) console.error(`\n${res.logs}`);
|
|
10539
|
+
if (!res.success) fail("Database connection failed.", "The connection log above (stderr) has the reason.", "db_connection_failed");
|
|
10540
|
+
success("Database connection succeeded");
|
|
10541
|
+
emit(() => {}, {
|
|
10542
|
+
success: true,
|
|
10543
|
+
projectId,
|
|
10544
|
+
logs: res.logs ?? null
|
|
10545
|
+
});
|
|
10546
|
+
} catch (e) {
|
|
10547
|
+
reportError(e, "Failed to test database");
|
|
10548
|
+
}
|
|
10262
10549
|
}
|
|
10263
10550
|
/**
|
|
10264
|
-
*
|
|
10551
|
+
* `rebase cloud db info [--reveal]` — where a project's database actually lives.
|
|
10265
10552
|
*
|
|
10266
|
-
*
|
|
10267
|
-
*
|
|
10268
|
-
*
|
|
10269
|
-
*
|
|
10553
|
+
* The password is NEVER in the default output; `--reveal` fetches it through the
|
|
10554
|
+
* separate reveal call, and it appears in JSON only when `--reveal` is given.
|
|
10555
|
+
* Any field the server could not resolve comes back `null` and is rendered as
|
|
10556
|
+
* unavailable, never a placeholder.
|
|
10270
10557
|
*/
|
|
10271
|
-
async function
|
|
10558
|
+
async function dbInfo(rawArgs) {
|
|
10559
|
+
const args = arg({
|
|
10560
|
+
"--reveal": Boolean,
|
|
10561
|
+
"--project": String,
|
|
10562
|
+
"-p": "--project"
|
|
10563
|
+
}, {
|
|
10564
|
+
argv: rawArgs.slice(2),
|
|
10565
|
+
permissive: true
|
|
10566
|
+
});
|
|
10567
|
+
const { client } = await requireClient(rawArgs);
|
|
10568
|
+
const projectId = await requireProject(rawArgs, client);
|
|
10569
|
+
const projectRef = displayProjectRef(rawArgs);
|
|
10272
10570
|
try {
|
|
10273
|
-
|
|
10571
|
+
const info = await client.functions.invoke("db-info", void 0, {
|
|
10274
10572
|
method: "GET",
|
|
10275
10573
|
path: projectId
|
|
10276
|
-
})
|
|
10277
|
-
|
|
10278
|
-
|
|
10279
|
-
|
|
10280
|
-
|
|
10281
|
-
|
|
10282
|
-
|
|
10283
|
-
|
|
10284
|
-
|
|
10285
|
-
const p = await client.data.collection("projects").findById(projectId);
|
|
10286
|
-
if (!p) fail(`Project ${projectRef} not found.`, void 0, "project_not_found");
|
|
10287
|
-
const [db, lastDeploy, baseDomain, capacity] = await Promise.all([
|
|
10288
|
-
firstRow(client, "databases", projectId),
|
|
10289
|
-
latestDeployment(client, projectId),
|
|
10290
|
-
fetchTenantBaseDomain(client, url),
|
|
10291
|
-
fetchCapacity(client, projectId)
|
|
10292
|
-
]);
|
|
10574
|
+
});
|
|
10575
|
+
let password;
|
|
10576
|
+
let connectionString;
|
|
10577
|
+
if (args["--reveal"]) {
|
|
10578
|
+
if (!info.passwordAvailable) fail("No password is available to reveal for this database.", info.unavailableReason ?? void 0, "password_unavailable");
|
|
10579
|
+
const revealed = await client.functions.invoke("db-info", { projectId }, { path: "reveal" });
|
|
10580
|
+
password = revealed.password;
|
|
10581
|
+
connectionString = revealed.connectionString;
|
|
10582
|
+
}
|
|
10293
10583
|
emit(() => {
|
|
10294
10584
|
console.log("");
|
|
10295
|
-
console.log(
|
|
10585
|
+
console.log(chalk.bold(` 🗄 Database — project ${projectRef}`) + chalk.gray(` (${info.type})`));
|
|
10296
10586
|
console.log("");
|
|
10297
10587
|
keyValues([
|
|
10298
|
-
["
|
|
10299
|
-
["
|
|
10300
|
-
["
|
|
10301
|
-
["
|
|
10302
|
-
["
|
|
10303
|
-
["
|
|
10304
|
-
["Organization", p.organization !== void 0 ? String(p.organization) : void 0],
|
|
10305
|
-
["Database", db ? `${db.type} (${colorStatus(db.connectionStatus)})` : "none"],
|
|
10306
|
-
["Storage", capacity && capacity.limitMb > 0 ? `${capacity.usedMb} MB / ${capacity.limitMb} MB${capacity.usedFraction !== null ? ` (${(capacity.usedFraction * 100).toFixed(0)}%)` : ""}` : void 0],
|
|
10307
|
-
["Last deploy", lastDeploy ? `${colorStatus(lastDeploy.status)} · ${fmtDate(lastDeploy.createdAt)}` : "never"]
|
|
10588
|
+
["Host", info.host],
|
|
10589
|
+
["Port", info.port],
|
|
10590
|
+
["Database", info.database],
|
|
10591
|
+
["Username", info.username],
|
|
10592
|
+
["Password", info.passwordAvailable ? password ?? chalk.gray("hidden — pass --reveal") : chalk.gray("unavailable")],
|
|
10593
|
+
["Connection", connectionString]
|
|
10308
10594
|
]);
|
|
10309
|
-
if (
|
|
10595
|
+
if (info.unavailableReason) console.log(chalk.gray(` ${info.unavailableReason}`));
|
|
10596
|
+
if (info.portForward) {
|
|
10597
|
+
const pf = info.portForward;
|
|
10310
10598
|
console.log("");
|
|
10311
|
-
console.log(
|
|
10312
|
-
console.log(` ${chalk.gray(capacity.detail)}`);
|
|
10599
|
+
console.log(chalk.gray(` Port-forward: kubectl -n ${pf.namespace} port-forward svc/${pf.service} ${pf.localPort}:${pf.remotePort}`));
|
|
10313
10600
|
}
|
|
10314
10601
|
console.log("");
|
|
10315
10602
|
}, {
|
|
10316
|
-
|
|
10317
|
-
|
|
10318
|
-
|
|
10319
|
-
|
|
10320
|
-
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
10324
|
-
|
|
10325
|
-
|
|
10326
|
-
|
|
10327
|
-
|
|
10328
|
-
|
|
10329
|
-
connectionStatus: db.connectionStatus ?? null,
|
|
10330
|
-
capacity: capacity ?? null
|
|
10331
|
-
} : null,
|
|
10332
|
-
lastDeploy: lastDeploy ? {
|
|
10333
|
-
id: String(lastDeploy.id),
|
|
10334
|
-
status: lastDeploy.status ?? null,
|
|
10335
|
-
createdAt: lastDeploy.createdAt ?? null
|
|
10336
|
-
} : null
|
|
10603
|
+
projectId,
|
|
10604
|
+
type: info.type,
|
|
10605
|
+
host: info.host,
|
|
10606
|
+
port: info.port,
|
|
10607
|
+
database: info.database,
|
|
10608
|
+
username: info.username,
|
|
10609
|
+
passwordAvailable: info.passwordAvailable,
|
|
10610
|
+
portForward: info.portForward,
|
|
10611
|
+
unavailableReason: info.unavailableReason,
|
|
10612
|
+
...args["--reveal"] ? {
|
|
10613
|
+
password,
|
|
10614
|
+
connectionString
|
|
10615
|
+
} : {}
|
|
10337
10616
|
});
|
|
10338
10617
|
} catch (e) {
|
|
10339
|
-
reportError(e, "Failed to load
|
|
10618
|
+
reportError(e, "Failed to load database info");
|
|
10340
10619
|
}
|
|
10341
10620
|
}
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10621
|
+
/**
|
|
10622
|
+
* `db backup [action] [filename]`, resolved in one strict parse.
|
|
10623
|
+
*
|
|
10624
|
+
* Both halves were reachable by the old operand filter, and both are
|
|
10625
|
+
* destructive: `rebase cloud db backup -p acme` read `--project`'s value as the
|
|
10626
|
+
* ACTION (falling through to a list, so the flag silently changed what ran),
|
|
10627
|
+
* and `db backup restore -p acme` read it as the FILENAME — a restore staged
|
|
10628
|
+
* over the live database, named after the project slug. An undeclared flag was
|
|
10629
|
+
* dropped instead of refused, which is the same failure one step quieter: `db
|
|
10630
|
+
* backup --dry-run` ran a list, having silently discarded the flag that was
|
|
10631
|
+
* supposed to change what it did.
|
|
10632
|
+
*
|
|
10633
|
+
* Exported so its tests drive the real parser.
|
|
10634
|
+
*/
|
|
10635
|
+
function resolveBackupArgs(rawArgs) {
|
|
10636
|
+
const { flags, positionals } = parseCloudArgs({
|
|
10637
|
+
spec: { "--yes": Boolean },
|
|
10345
10638
|
rawArgs,
|
|
10346
10639
|
commandWords: 3,
|
|
10347
|
-
command: "cloud
|
|
10348
|
-
maxPositionals:
|
|
10640
|
+
command: "cloud db backup",
|
|
10641
|
+
maxPositionals: 2
|
|
10349
10642
|
});
|
|
10643
|
+
return {
|
|
10644
|
+
flags,
|
|
10645
|
+
action: positionals[0] || "list",
|
|
10646
|
+
filename: positionals[1]
|
|
10647
|
+
};
|
|
10648
|
+
}
|
|
10649
|
+
async function backupCommand(rawArgs) {
|
|
10650
|
+
const { flags: args, action, filename: backupFile } = resolveBackupArgs(rawArgs);
|
|
10350
10651
|
const { client } = await requireClient(rawArgs);
|
|
10351
|
-
const projectId = await
|
|
10352
|
-
const
|
|
10353
|
-
if (!p) fail(`Project ${projectRef} not found.`, void 0, "project_not_found");
|
|
10354
|
-
await confirmDestructive({
|
|
10355
|
-
yes: Boolean(args["--yes"]),
|
|
10356
|
-
prompt: `Permanently delete project "${p.name ?? projectRef}" (${p.subdomain ?? projectRef})? This tears down its deployment.`
|
|
10357
|
-
});
|
|
10652
|
+
const projectId = await requireProject(rawArgs, client);
|
|
10653
|
+
const projectRef = displayProjectRef(rawArgs);
|
|
10358
10654
|
try {
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10655
|
+
if (action === "create") {
|
|
10656
|
+
const res = await client.functions.invoke("backup", {
|
|
10657
|
+
projectId,
|
|
10658
|
+
type: "manual"
|
|
10659
|
+
}, { path: "create" });
|
|
10660
|
+
if (!res.success) fail(res.error || "Backup failed.");
|
|
10661
|
+
emit(() => success(`Backup created: ${res.backup?.filename ?? "(unknown)"}`), {
|
|
10662
|
+
success: true,
|
|
10663
|
+
backup: res.backup ?? null
|
|
10664
|
+
});
|
|
10665
|
+
return;
|
|
10666
|
+
}
|
|
10667
|
+
if (action === "restore") {
|
|
10668
|
+
const filename = backupFile;
|
|
10669
|
+
if (!filename) fail("Usage: rebase cloud db backup restore <filename>", void 0, "usage");
|
|
10670
|
+
await confirmDestructive({
|
|
10671
|
+
yes: Boolean(args["--yes"]),
|
|
10672
|
+
prompt: `Restore "${filename}" over the current database for project ${projectRef}?`
|
|
10673
|
+
});
|
|
10674
|
+
const res = await client.functions.invoke("backup", {
|
|
10675
|
+
projectId,
|
|
10676
|
+
filename
|
|
10677
|
+
}, { path: "restore" });
|
|
10678
|
+
if (!res.success) fail(res.error || "Restore failed.");
|
|
10679
|
+
emit(() => success(res.message || "Restore complete"), {
|
|
10680
|
+
success: true,
|
|
10681
|
+
message: res.message ?? null
|
|
10682
|
+
});
|
|
10683
|
+
return;
|
|
10684
|
+
}
|
|
10685
|
+
if (action === "status") {
|
|
10686
|
+
const res = await client.functions.invoke("backup", void 0, {
|
|
10687
|
+
method: "GET",
|
|
10688
|
+
path: `backup-status/${projectId}`
|
|
10689
|
+
});
|
|
10690
|
+
emit(() => {
|
|
10691
|
+
console.log("");
|
|
10692
|
+
console.log(chalk.bold(` 💾 Automated backups — project ${projectRef}`));
|
|
10693
|
+
console.log("");
|
|
10694
|
+
keyValues([
|
|
10695
|
+
["Enabled", res.enabled ? chalk.green("yes") : chalk.yellow("no")],
|
|
10696
|
+
["Reason", String(res.reason ?? "")],
|
|
10697
|
+
["Database type", String(res.databaseType ?? "")],
|
|
10698
|
+
["Last backup", res.lastSuccessfulBackup ?? void 0],
|
|
10699
|
+
["Recovery window", res.recoveryWindow ? `${res.recoveryWindow.from} → ${res.recoveryWindow.to}` : void 0]
|
|
10700
|
+
]);
|
|
10701
|
+
console.log("");
|
|
10702
|
+
}, res);
|
|
10703
|
+
return;
|
|
10704
|
+
}
|
|
10705
|
+
if (action === "download") {
|
|
10706
|
+
const filename = backupFile;
|
|
10707
|
+
if (!filename) fail("Usage: rebase cloud db backup download <filename>", void 0, "usage");
|
|
10708
|
+
const res = await client.functions.invoke("backup", void 0, {
|
|
10709
|
+
method: "GET",
|
|
10710
|
+
path: `download/${projectId}/${encodeURIComponent(filename)}`
|
|
10711
|
+
});
|
|
10712
|
+
emit(() => {
|
|
10713
|
+
console.log("");
|
|
10714
|
+
console.log(chalk.bold(` ${res.name}`) + chalk.gray(` ${(res.size / 1024 / 1024).toFixed(1)} MB`));
|
|
10715
|
+
console.log(` ${chalk.cyan(res.url)}`);
|
|
10716
|
+
console.log("");
|
|
10717
|
+
console.log(chalk.gray(" Short-lived signed URL — fetch it with curl/wget."));
|
|
10718
|
+
console.log("");
|
|
10719
|
+
}, {
|
|
10720
|
+
name: res.name,
|
|
10721
|
+
size: res.size,
|
|
10722
|
+
url: res.url
|
|
10723
|
+
});
|
|
10724
|
+
return;
|
|
10725
|
+
}
|
|
10726
|
+
const res = await client.functions.invoke("backup", void 0, {
|
|
10727
|
+
method: "GET",
|
|
10728
|
+
path: `list/${projectId}`
|
|
10729
|
+
});
|
|
10730
|
+
emit(() => {
|
|
10731
|
+
console.log("");
|
|
10732
|
+
console.log(chalk.bold(` 💾 Backups — project ${projectRef}`));
|
|
10733
|
+
console.log("");
|
|
10734
|
+
if (!res.backups?.length) {
|
|
10735
|
+
console.log(chalk.gray(" No backups yet. Create one with `rebase cloud db backup create`."));
|
|
10736
|
+
console.log("");
|
|
10737
|
+
return;
|
|
10738
|
+
}
|
|
10739
|
+
for (const b of res.backups) {
|
|
10740
|
+
const size = b.size !== void 0 ? `${(b.size / 1024 / 1024).toFixed(1)} MB` : "";
|
|
10741
|
+
console.log(` ${chalk.bold(b.filename)} ${chalk.gray(`${b.type ?? ""} ${size}`.trim())}`);
|
|
10742
|
+
}
|
|
10743
|
+
console.log("");
|
|
10744
|
+
}, {
|
|
10745
|
+
projectId,
|
|
10746
|
+
backups: res.backups ?? []
|
|
10366
10747
|
});
|
|
10367
10748
|
} catch (e) {
|
|
10368
|
-
reportError(e, "
|
|
10749
|
+
reportError(e, "Backup operation failed");
|
|
10369
10750
|
}
|
|
10370
10751
|
}
|
|
10371
|
-
async function firstRow(client, collection, projectId) {
|
|
10372
|
-
return (await client.data.collection(collection).find({
|
|
10373
|
-
where: { project: ["==", projectId] },
|
|
10374
|
-
limit: 1
|
|
10375
|
-
})).data[0];
|
|
10376
|
-
}
|
|
10377
|
-
async function latestDeployment(client, projectId) {
|
|
10378
|
-
return (await client.data.collection("deployments").find({
|
|
10379
|
-
where: { project: ["==", projectId] },
|
|
10380
|
-
orderBy: ["createdAt", "desc"],
|
|
10381
|
-
limit: 1
|
|
10382
|
-
})).data[0];
|
|
10383
|
-
}
|
|
10384
|
-
function fmtDate(value) {
|
|
10385
|
-
if (!value) return "—";
|
|
10386
|
-
const d = new Date(value);
|
|
10387
|
-
return isNaN(d.getTime()) ? value : d.toLocaleString();
|
|
10388
|
-
}
|
|
10389
|
-
//#endregion
|
|
10390
|
-
//#region src/commands/cloud/bundle-deploy.ts
|
|
10391
10752
|
/**
|
|
10392
|
-
*
|
|
10393
|
-
*
|
|
10394
|
-
* `rebase cloud deploy --bundle` builds the bundle, tars it, uploads it to the
|
|
10395
|
-
* control plane's bundle endpoint, and triggers a deploy carrying the bundle id
|
|
10396
|
-
* and its generated manifest. The control plane resolves a runtime from the
|
|
10397
|
-
* manifest's range and runs the platform image with this bundle — the managed
|
|
10398
|
-
* path. A project not in managed mode, or one whose bundle fails intake, is told
|
|
10399
|
-
* so by the control plane; this side just packages and hands it over.
|
|
10753
|
+
* `rebase cloud db pitr <status|restore|cutover|discard>`.
|
|
10400
10754
|
*
|
|
10401
|
-
*
|
|
10402
|
-
*
|
|
10403
|
-
*
|
|
10755
|
+
* A PITR restore is STAGED, not applied: `restore` creates a recovered copy of
|
|
10756
|
+
* the database beside the live one — the application is NOT repointed and the
|
|
10757
|
+
* original is left running and unchanged. `cutover` is the separate, explicit
|
|
10758
|
+
* step that repoints the app at the recovered copy (and restarts it). `discard`
|
|
10759
|
+
* removes a staged copy; the server refuses to discard a copy that has been cut
|
|
10760
|
+
* over to (it is now the live database). Every mutating step requires `--yes` in
|
|
10761
|
+
* non-interactive use, and the CLI surfaces these staged semantics honestly.
|
|
10404
10762
|
*/
|
|
10405
|
-
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10763
|
+
async function pitrCommand(rawArgs) {
|
|
10764
|
+
const { flags: args, positionals } = parseCloudArgs({
|
|
10765
|
+
spec: {
|
|
10766
|
+
"--target": String,
|
|
10767
|
+
"--yes": Boolean
|
|
10768
|
+
},
|
|
10769
|
+
rawArgs,
|
|
10770
|
+
commandWords: 3,
|
|
10771
|
+
command: "cloud db pitr",
|
|
10772
|
+
maxPositionals: 1
|
|
10773
|
+
});
|
|
10774
|
+
const action = positionals[0] || "status";
|
|
10775
|
+
const { client } = await requireClient(rawArgs);
|
|
10776
|
+
const projectId = await requireProject(rawArgs, client);
|
|
10777
|
+
const projectRef = displayProjectRef(rawArgs);
|
|
10410
10778
|
try {
|
|
10411
|
-
|
|
10412
|
-
|
|
10413
|
-
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
10420
|
-
|
|
10421
|
-
|
|
10422
|
-
|
|
10423
|
-
|
|
10424
|
-
|
|
10425
|
-
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
|
|
10430
|
-
|
|
10431
|
-
"--
|
|
10432
|
-
"--
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
|
|
10440
|
-
|
|
10441
|
-
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10779
|
+
if (action === "status") {
|
|
10780
|
+
const res = await client.functions.invoke("backup", void 0, {
|
|
10781
|
+
method: "GET",
|
|
10782
|
+
path: `pitr-status/${projectId}`
|
|
10783
|
+
});
|
|
10784
|
+
emit(() => {
|
|
10785
|
+
console.log("");
|
|
10786
|
+
console.log(chalk.bold(` ⏱ Point-in-time recovery — project ${projectRef}`));
|
|
10787
|
+
console.log("");
|
|
10788
|
+
keyValues([
|
|
10789
|
+
["Available", res.available ? chalk.green("yes") : chalk.yellow("no")],
|
|
10790
|
+
["First recoverable", res.firstRecoverabilityPoint ?? void 0],
|
|
10791
|
+
["Last backup", res.lastSuccessfulBackup ?? void 0],
|
|
10792
|
+
["Message", res.message ?? void 0]
|
|
10793
|
+
]);
|
|
10794
|
+
console.log("");
|
|
10795
|
+
}, res);
|
|
10796
|
+
return;
|
|
10797
|
+
}
|
|
10798
|
+
if (action === "restore") {
|
|
10799
|
+
const target = args["--target"];
|
|
10800
|
+
if (!target) fail("Usage: rebase cloud db pitr restore --target <ISO timestamp>", void 0, "usage");
|
|
10801
|
+
await confirmDestructive({
|
|
10802
|
+
yes: Boolean(args["--yes"]),
|
|
10803
|
+
prompt: `Stage a point-in-time recovery of project ${projectRef} at ${target}? (stages a copy; does not repoint your app)`
|
|
10804
|
+
});
|
|
10805
|
+
const res = await client.functions.invoke("backup", {
|
|
10806
|
+
projectId,
|
|
10807
|
+
targetTime: target,
|
|
10808
|
+
acknowledgeNoCutover: true
|
|
10809
|
+
}, { path: "pitr-restore" });
|
|
10810
|
+
emit(() => {
|
|
10811
|
+
console.log("");
|
|
10812
|
+
console.log(chalk.yellow(` ⏳ ${String(res.message ?? "Recovery staged.")}`));
|
|
10813
|
+
console.log(chalk.gray(" Watch progress with `rebase cloud db pitr status`, then `rebase cloud db pitr cutover --yes`."));
|
|
10814
|
+
console.log("");
|
|
10815
|
+
}, res);
|
|
10816
|
+
return;
|
|
10817
|
+
}
|
|
10818
|
+
if (action === "cutover") {
|
|
10819
|
+
await confirmDestructive({
|
|
10820
|
+
yes: Boolean(args["--yes"]),
|
|
10821
|
+
prompt: `Cut project ${projectRef} over to the staged recovery? This repoints and restarts your application.`
|
|
10822
|
+
});
|
|
10823
|
+
const res = await client.functions.invoke("backup", { projectId }, { path: "pitr-restore-cutover" });
|
|
10824
|
+
emit(() => {
|
|
10825
|
+
console.log("");
|
|
10826
|
+
console.log(String(res.message ?? "Cutover requested."));
|
|
10827
|
+
console.log("");
|
|
10828
|
+
}, res);
|
|
10829
|
+
return;
|
|
10830
|
+
}
|
|
10831
|
+
if (action === "discard") {
|
|
10832
|
+
await confirmDestructive({
|
|
10833
|
+
yes: Boolean(args["--yes"]),
|
|
10834
|
+
prompt: `Discard the staged recovery for project ${projectRef}? This deletes the staged copy and its storage.`
|
|
10835
|
+
});
|
|
10836
|
+
const res = await client.functions.invoke("backup", { projectId }, { path: "pitr-restore-discard" });
|
|
10837
|
+
emit(() => success(String(res.message ?? "Staged restore discarded.")), res);
|
|
10838
|
+
return;
|
|
10839
|
+
}
|
|
10840
|
+
fail(`Unknown pitr command: ${action}`, "Try status | restore | cutover | discard.", "usage");
|
|
10841
|
+
} catch (e) {
|
|
10842
|
+
reportError(e, "PITR operation failed");
|
|
10843
|
+
}
|
|
10844
|
+
}
|
|
10845
|
+
function printDbHelp() {
|
|
10846
|
+
emitHelp("db", [
|
|
10847
|
+
"list",
|
|
10848
|
+
"create",
|
|
10849
|
+
"info",
|
|
10850
|
+
"test",
|
|
10851
|
+
"backup",
|
|
10852
|
+
"pitr"
|
|
10853
|
+
], () => {
|
|
10854
|
+
console.log(`
|
|
10855
|
+
${chalk.bold("rebase cloud db")} — Database & backups
|
|
10856
|
+
|
|
10857
|
+
${chalk.green.bold("Commands")}
|
|
10858
|
+
${chalk.blue.bold("list")} List databases attached to the project
|
|
10859
|
+
${chalk.blue.bold("create")} Attach a managed or bring-your-own database
|
|
10860
|
+
${chalk.blue.bold("info")} ${chalk.gray("[--reveal]")} Connection details ${chalk.gray("(password only with --reveal)")}
|
|
10861
|
+
${chalk.blue.bold("test")} Test database connectivity
|
|
10862
|
+
${chalk.blue.bold("backup list")} List backups
|
|
10863
|
+
${chalk.blue.bold("backup create")} Create a manual backup
|
|
10864
|
+
${chalk.blue.bold("backup restore")} ${chalk.gray("<file>")} Restore a backup
|
|
10865
|
+
${chalk.blue.bold("backup status")} Automated-backup health
|
|
10866
|
+
${chalk.blue.bold("backup download")} ${chalk.gray("<file>")} Signed URL for a backup
|
|
10867
|
+
${chalk.blue.bold("pitr status")} Point-in-time recovery window
|
|
10868
|
+
${chalk.blue.bold("pitr restore")} ${chalk.gray("--target <ISO>")} Stage a recovery ${chalk.gray("(does not repoint)")}
|
|
10869
|
+
${chalk.blue.bold("pitr cutover")} ${chalk.gray("-y")} Repoint the app at the staged recovery
|
|
10870
|
+
${chalk.blue.bold("pitr discard")} ${chalk.gray("-y")} Delete a staged recovery
|
|
10871
|
+
|
|
10872
|
+
${chalk.green.bold("Options")}
|
|
10873
|
+
${chalk.blue("--project, -p")} Project slug ${chalk.gray("(defaults to the linked project)")}
|
|
10874
|
+
${chalk.blue("--reveal")} Include the DB password ${chalk.gray("(info)")}
|
|
10875
|
+
${chalk.blue("--type")} managed | byodb ${chalk.gray("(create)")}
|
|
10876
|
+
${chalk.blue("--connection-string")} External DB URL ${chalk.gray("(byodb)")}
|
|
10877
|
+
${chalk.blue("--json")} Machine-readable output
|
|
10878
|
+
`);
|
|
10446
10879
|
});
|
|
10447
10880
|
}
|
|
10881
|
+
//#endregion
|
|
10882
|
+
//#region src/commands/cloud/projects.ts
|
|
10448
10883
|
/**
|
|
10449
|
-
*
|
|
10450
|
-
*
|
|
10451
|
-
* The manifest travels with the trigger so the control plane can validate intake
|
|
10452
|
-
* without unpacking the uploaded archive first — a rejection (native deps, no
|
|
10453
|
-
* matching runtime) is then a fast, cheap answer.
|
|
10884
|
+
* `rebase cloud projects` — list / create / info / delete.
|
|
10454
10885
|
*/
|
|
10455
|
-
function
|
|
10456
|
-
|
|
10457
|
-
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
10886
|
+
async function listProjects(rawArgs) {
|
|
10887
|
+
const { client, url } = await requireClient(rawArgs);
|
|
10888
|
+
const org = getContextOrg(url);
|
|
10889
|
+
try {
|
|
10890
|
+
const [projects, baseDomain] = await Promise.all([client.data.collection("projects").find({
|
|
10891
|
+
where: org ? { organization: ["==", org] } : void 0,
|
|
10892
|
+
orderBy: ["name", "asc"],
|
|
10893
|
+
limit: 100
|
|
10894
|
+
}).then((res) => res.data), fetchTenantBaseDomain(client, url)]);
|
|
10895
|
+
const linkedId = readLink()?.projectId;
|
|
10896
|
+
emit(() => {
|
|
10897
|
+
console.log("");
|
|
10898
|
+
console.log(chalk.bold(" 📦 Projects") + (org ? chalk.gray(` (org ${org})`) : ""));
|
|
10899
|
+
console.log("");
|
|
10900
|
+
if (projects.length === 0) {
|
|
10901
|
+
console.log(chalk.gray(" No projects yet. Create one with `rebase cloud projects create`."));
|
|
10902
|
+
console.log("");
|
|
10903
|
+
return;
|
|
10904
|
+
}
|
|
10905
|
+
for (const p of projects) {
|
|
10906
|
+
const marker = String(p.id) === linkedId ? chalk.green(" ●") : " ";
|
|
10907
|
+
console.log(`${marker}${chalk.bold(p.name ?? "(unnamed)")} ${chalk.gray(`[${p.subdomain ?? p.id}]`)} ${colorStatus(p.status)}`);
|
|
10908
|
+
console.log(` ${chalk.gray(projectHost(p, baseDomain) ?? "—")}${p.provider ? chalk.gray(` · ${p.provider}`) : ""}`);
|
|
10909
|
+
}
|
|
10910
|
+
console.log("");
|
|
10911
|
+
}, {
|
|
10912
|
+
org: org ?? null,
|
|
10913
|
+
projects: projects.map((p) => ({
|
|
10914
|
+
id: String(p.id),
|
|
10915
|
+
name: p.name ?? null,
|
|
10916
|
+
slug: p.subdomain ?? null,
|
|
10917
|
+
host: projectHost(p, baseDomain) ?? null,
|
|
10918
|
+
status: p.status ?? null,
|
|
10919
|
+
provider: p.provider ?? null,
|
|
10920
|
+
linked: String(p.id) === linkedId
|
|
10921
|
+
}))
|
|
10922
|
+
});
|
|
10923
|
+
} catch (e) {
|
|
10924
|
+
reportError(e, "Failed to list projects");
|
|
10925
|
+
}
|
|
10466
10926
|
}
|
|
10467
10927
|
/**
|
|
10468
|
-
*
|
|
10928
|
+
* Default region per provider. Required on create; the rest is dialled.
|
|
10469
10929
|
*
|
|
10470
|
-
*
|
|
10471
|
-
*
|
|
10472
|
-
*
|
|
10473
|
-
*
|
|
10474
|
-
*
|
|
10475
|
-
* status says what is actually true.
|
|
10930
|
+
* There used to be a `vmSize` here too — `e2-small`, `cx21` — naming a machine
|
|
10931
|
+
* on a price list this platform does not buy from. The column was dropped on
|
|
10932
|
+
* 2026-08-20 and this went on sending it, which a control plane can only ignore
|
|
10933
|
+
* or refuse. What a project reserves is a set of dials now, and a project that
|
|
10934
|
+
* sets none takes the platform default.
|
|
10476
10935
|
*/
|
|
10477
|
-
function
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
type: value?.type === "backend" ? "backend" : "static"
|
|
10483
|
-
}));
|
|
10484
|
-
}
|
|
10485
|
-
/** Upload a bundle archive; returns the control-plane bundle id. */
|
|
10486
|
-
async function uploadBundle(url, token, projectId, tarPath) {
|
|
10487
|
-
const bytes = fs.readFileSync(tarPath);
|
|
10488
|
-
const res = await fetch(`${url}/api/functions/deploy/bundle/upload?projectId=${encodeURIComponent(projectId)}`, {
|
|
10489
|
-
method: "POST",
|
|
10490
|
-
headers: {
|
|
10491
|
-
Authorization: `Bearer ${token}`,
|
|
10492
|
-
"Content-Type": "application/gzip"
|
|
10493
|
-
},
|
|
10494
|
-
body: bytes
|
|
10495
|
-
});
|
|
10496
|
-
if (!res.ok) {
|
|
10497
|
-
const body = await res.text().catch(() => "");
|
|
10498
|
-
throw new Error(`Bundle upload failed (${res.status}): ${body || res.statusText}`);
|
|
10936
|
+
function providerDefaults(provider) {
|
|
10937
|
+
switch (provider) {
|
|
10938
|
+
case "gcp": return { region: "europe-west1" };
|
|
10939
|
+
case "aws": return { region: "us-east-1" };
|
|
10940
|
+
default: return { region: "nbg1" };
|
|
10499
10941
|
}
|
|
10500
|
-
const data = await res.json();
|
|
10501
|
-
if (!data.bundleId) throw new Error("Bundle upload endpoint did not return a bundle id.");
|
|
10502
|
-
return data.bundleId;
|
|
10503
10942
|
}
|
|
10504
|
-
//#endregion
|
|
10505
|
-
//#region src/commands/cloud/deploy.ts
|
|
10506
10943
|
/**
|
|
10507
|
-
*
|
|
10944
|
+
* Where this project says it runs.
|
|
10508
10945
|
*
|
|
10509
|
-
* `
|
|
10510
|
-
*
|
|
10511
|
-
*
|
|
10946
|
+
* `provider`/`region` are a *request*: no code downstream reads them to pick a
|
|
10947
|
+
* deploy target — that comes from the project's cluster record or the ambient
|
|
10948
|
+
* in-cluster context (saas/backend/src/k8s/resolve.ts). So a wrong value here is
|
|
10949
|
+
* never contradicted by a failure; it just sits in the record. The CLI used to
|
|
10950
|
+
* default to `hetzner`/`nbg1` unconditionally, which is how projects running on
|
|
10951
|
+
* our GKE cluster came to describe themselves as Hetzner in the console — and
|
|
10952
|
+
* `provider` also decides which substrate's rules a project's dials are clamped
|
|
10953
|
+
* to — Autopilot's 250m floor and 1:1-6.5:1 band do not apply on Hetzner or EKS
|
|
10954
|
+
* — so a wrong value here resizes pods, it is not a cosmetic slip.
|
|
10512
10955
|
*
|
|
10513
|
-
*
|
|
10514
|
-
*
|
|
10515
|
-
*
|
|
10516
|
-
*
|
|
10517
|
-
*
|
|
10956
|
+
* The control plane already publishes the infrastructure that actually exists,
|
|
10957
|
+
* and the console's create wizard reads it. Ask the same question here.
|
|
10958
|
+
*
|
|
10959
|
+
* Exported for tests: the decision is pure, so it can be pinned without a
|
|
10960
|
+
* control plane. The fetching and the exit live in `resolveRequestedTarget`.
|
|
10961
|
+
*
|
|
10962
|
+
* @param requested `--provider`, if the caller named one. An explicit flag wins:
|
|
10963
|
+
* it is the caller stating intent, and `deploy` corrects the record anyway.
|
|
10964
|
+
* @param targets What the control plane says exists, or `undefined` when it
|
|
10965
|
+
* cannot say — an older deployment with no `platform-config`, or a failed
|
|
10966
|
+
* request. That is different from an empty list, which is a control plane
|
|
10967
|
+
* stating it has no infrastructure at all.
|
|
10968
|
+
* @returns the target to record, or `null` when the control plane answered that
|
|
10969
|
+
* there is none.
|
|
10518
10970
|
*/
|
|
10519
|
-
|
|
10520
|
-
|
|
10521
|
-
|
|
10522
|
-
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10971
|
+
function chooseRequestedTarget(requested, targets) {
|
|
10972
|
+
if (requested) return {
|
|
10973
|
+
provider: requested,
|
|
10974
|
+
region: void 0
|
|
10975
|
+
};
|
|
10976
|
+
if (!targets) return {
|
|
10977
|
+
provider: "hetzner",
|
|
10978
|
+
region: void 0
|
|
10979
|
+
};
|
|
10980
|
+
if (targets.length === 0) return null;
|
|
10981
|
+
const [target] = targets;
|
|
10982
|
+
return {
|
|
10983
|
+
provider: target.provider,
|
|
10984
|
+
region: target.region?.trim() || void 0
|
|
10985
|
+
};
|
|
10986
|
+
}
|
|
10987
|
+
async function resolveRequestedTarget(client, url, requested) {
|
|
10988
|
+
const chosen = chooseRequestedTarget(requested, await fetchDeployTargets(client, url));
|
|
10989
|
+
if (!chosen) fail("This control plane has no deploy targets configured.", `Register a cluster, or pass ${chalk.bold("--provider")} and ${chalk.bold("--region")} to record one anyway.`, "no_deploy_targets");
|
|
10990
|
+
return chosen;
|
|
10991
|
+
}
|
|
10992
|
+
/** The flags `rebase cloud projects create` takes. */
|
|
10993
|
+
var CREATE_PROJECT_FLAGS = {
|
|
10994
|
+
"--name": String,
|
|
10995
|
+
"--subdomain": String,
|
|
10996
|
+
"--repo": String,
|
|
10997
|
+
"--branch": String,
|
|
10998
|
+
"--provider": String,
|
|
10999
|
+
"--region": String,
|
|
11000
|
+
"--org": String,
|
|
11001
|
+
"--link": Boolean,
|
|
11002
|
+
/**
|
|
11003
|
+
* Which database the new project gets — `managed` (the default), `byodb`,
|
|
11004
|
+
* or `none`.
|
|
11005
|
+
*
|
|
11006
|
+
* A default rather than a prompt, and `managed` rather than `none`, because
|
|
11007
|
+
* the state this removes is not a missing convenience: a project with no
|
|
11008
|
+
* database is written `status: "provisioning"` and can never deploy, and
|
|
11009
|
+
* nothing in that word says a second command is owed. Attaching one here
|
|
11010
|
+
* means the two-command sequence that every project needs is one command,
|
|
11011
|
+
* and `--db none` is there for the case that genuinely wants to decide later.
|
|
11012
|
+
*
|
|
11013
|
+
* Distinct from `--db-mode`/`--db-cpu` next to it, which are resource dials
|
|
11014
|
+
* on a database that exists. This is whether there is one.
|
|
11015
|
+
*/
|
|
11016
|
+
"--db": String,
|
|
11017
|
+
/** For `--db byodb`. Same spelling as `rebase cloud db create` uses. */
|
|
11018
|
+
"--connection-string": String,
|
|
11019
|
+
"-n": "--name",
|
|
11020
|
+
"--cpu": String,
|
|
11021
|
+
"--memory": String,
|
|
11022
|
+
"--replicas": String,
|
|
11023
|
+
"--spot": String,
|
|
11024
|
+
"--scale-to-zero": String,
|
|
11025
|
+
"--db-mode": String,
|
|
11026
|
+
"--db-instances": String,
|
|
11027
|
+
"--db-cpu": String,
|
|
11028
|
+
"--db-memory": String,
|
|
11029
|
+
"--storage": String
|
|
11030
|
+
};
|
|
11031
|
+
async function createProject(rawArgs) {
|
|
11032
|
+
const { flags: args } = parseCloudArgs({
|
|
11033
|
+
spec: CREATE_PROJECT_FLAGS,
|
|
11034
|
+
rawArgs,
|
|
11035
|
+
commandWords: 3,
|
|
11036
|
+
command: "cloud projects create",
|
|
11037
|
+
maxPositionals: 0
|
|
11038
|
+
});
|
|
11039
|
+
const { client, url } = await requireClient(rawArgs);
|
|
11040
|
+
const org = args["--org"] || getContextOrg(url);
|
|
11041
|
+
if (!org) fail("No organization selected.", `Pass ${chalk.bold("--org <id>")} or run ${chalk.bold("rebase cloud use")}.`, "no_org");
|
|
11042
|
+
const prompts = [];
|
|
11043
|
+
if (!args["--name"]) prompts.push({
|
|
11044
|
+
type: "input",
|
|
11045
|
+
name: "name",
|
|
11046
|
+
message: "Project name:"
|
|
10543
11047
|
});
|
|
11048
|
+
if (!args["--subdomain"]) prompts.push({
|
|
11049
|
+
type: "input",
|
|
11050
|
+
name: "subdomain",
|
|
11051
|
+
message: "Subdomain:"
|
|
11052
|
+
});
|
|
11053
|
+
const a = prompts.length && process.stdin.isTTY ? await inquirer.prompt(prompts) : {};
|
|
11054
|
+
const name = (args["--name"] || a.name || "").trim();
|
|
11055
|
+
const subdomain = (args["--subdomain"] || a.subdomain || "").trim().toLowerCase();
|
|
11056
|
+
const gitRepoUrl = (args["--repo"] || a.repo || "").trim();
|
|
11057
|
+
const gitBranch = (args["--branch"] || a.branch || "main").trim();
|
|
11058
|
+
const target = await resolveRequestedTarget(client, url, (args["--provider"] || a.provider)?.trim() || void 0);
|
|
11059
|
+
const provider = target.provider;
|
|
11060
|
+
const defaults = providerDefaults(provider);
|
|
11061
|
+
const region = (args["--region"] || target.region || defaults.region).trim();
|
|
11062
|
+
const dials = buildDialPatch(rawArgs, { requireOne: false });
|
|
11063
|
+
if (dials.error) fail(dials.error, void 0, "bad_request");
|
|
11064
|
+
const dbChoice = (args["--db"] ?? "managed").trim().toLowerCase();
|
|
11065
|
+
if (![
|
|
11066
|
+
"managed",
|
|
11067
|
+
"byodb",
|
|
11068
|
+
"none"
|
|
11069
|
+
].includes(dbChoice)) fail(`--db must be managed, byodb or none (got "${args["--db"]}").`, void 0, "bad_request");
|
|
11070
|
+
if (dbChoice === "byodb" && !args["--connection-string"]) fail("--db byodb needs the database to point at.", `Pass ${chalk.bold("--connection-string <url>")}.`, "input_required");
|
|
11071
|
+
if (!name || !subdomain) fail("Name and subdomain are required.", `Pass ${chalk.bold("--name <name>")} and ${chalk.bold("--subdomain <slug>")}.`, "input_required");
|
|
11072
|
+
try {
|
|
11073
|
+
const check = await client.functions.invoke("check-subdomain", { subdomain });
|
|
11074
|
+
if (!check.available) fail(`Subdomain "${subdomain}" is not available${check.reason ? ` (${check.reason})` : ""}.`, void 0, "subdomain_unavailable");
|
|
11075
|
+
} catch {}
|
|
11076
|
+
try {
|
|
11077
|
+
const user = await client.auth.getUser();
|
|
11078
|
+
if (!user) fail("Session is no longer valid.", "Run `rebase cloud login` again.", "session_invalid");
|
|
11079
|
+
const created = await client.data.collection("projects").create({
|
|
11080
|
+
name,
|
|
11081
|
+
subdomain,
|
|
11082
|
+
gitRepoUrl,
|
|
11083
|
+
gitBranch,
|
|
11084
|
+
provider,
|
|
11085
|
+
region,
|
|
11086
|
+
...dials.patch,
|
|
11087
|
+
organization: org,
|
|
11088
|
+
createdById: user.uid,
|
|
11089
|
+
status: "provisioning"
|
|
11090
|
+
});
|
|
11091
|
+
const host = projectHost(created, await fetchTenantBaseDomain(client, url));
|
|
11092
|
+
const linked = Boolean(args["--link"]);
|
|
11093
|
+
if (linked) writeLink({
|
|
11094
|
+
url,
|
|
11095
|
+
projectId: String(created.id),
|
|
11096
|
+
slug: created.subdomain,
|
|
11097
|
+
projectName: name,
|
|
11098
|
+
orgId: String(org)
|
|
11099
|
+
});
|
|
11100
|
+
const database = await attachRequestedDatabase(client, {
|
|
11101
|
+
projectId: String(created.id),
|
|
11102
|
+
choice: dbChoice,
|
|
11103
|
+
connectionString: args["--connection-string"],
|
|
11104
|
+
projectRef: created.subdomain ?? String(created.id)
|
|
11105
|
+
});
|
|
11106
|
+
if (database.warning) warn(database.warning[0], database.warning[1]);
|
|
11107
|
+
success(`Created project ${chalk.bold(name)}`);
|
|
11108
|
+
emit(() => {
|
|
11109
|
+
keyValues([
|
|
11110
|
+
["Slug", String(created.subdomain ?? "")],
|
|
11111
|
+
["URL", host],
|
|
11112
|
+
["Provider", provider],
|
|
11113
|
+
["Branch", gitBranch],
|
|
11114
|
+
["Database", database.line]
|
|
11115
|
+
]);
|
|
11116
|
+
if (linked) note(chalk.gray("Linked this directory to the new project."));
|
|
11117
|
+
noteBlank();
|
|
11118
|
+
for (const line of database.notes) note(chalk.gray(line));
|
|
11119
|
+
note(chalk.gray(`Deploy it with: ${chalk.bold(`rebase cloud deploy --project ${created.subdomain ?? created.id}`)}`));
|
|
11120
|
+
noteBlank();
|
|
11121
|
+
}, {
|
|
11122
|
+
success: true,
|
|
11123
|
+
id: String(created.id),
|
|
11124
|
+
name,
|
|
11125
|
+
slug: created.subdomain ?? null,
|
|
11126
|
+
host: host ?? null,
|
|
11127
|
+
provider,
|
|
11128
|
+
region,
|
|
11129
|
+
dials: dials.patch,
|
|
11130
|
+
branch: gitBranch,
|
|
11131
|
+
org: String(org),
|
|
11132
|
+
linked,
|
|
11133
|
+
database: database.payload
|
|
11134
|
+
});
|
|
11135
|
+
} catch (e) {
|
|
11136
|
+
reportError(e, "Failed to create project");
|
|
11137
|
+
}
|
|
10544
11138
|
}
|
|
10545
11139
|
/**
|
|
10546
|
-
*
|
|
10547
|
-
*
|
|
11140
|
+
* Attach the database `--db` asked for, and describe what happened.
|
|
11141
|
+
*
|
|
11142
|
+
* Never throws. The project already exists by the time this runs, so a failure
|
|
11143
|
+
* here is a *partial* success and has to be reported as one — with the exact
|
|
11144
|
+
* command that finishes the job. Turning it into an exception would report the
|
|
11145
|
+
* whole `projects create` as failed and leave a real project behind, which is
|
|
11146
|
+
* the worst of both readings.
|
|
10548
11147
|
*/
|
|
10549
|
-
async function
|
|
10550
|
-
|
|
10551
|
-
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
|
|
10559
|
-
for (const ignore of [".gitignore", ".rebaseignore"]) if (fs.existsSync(path.join(dir, ignore))) tarArgs.push(`--exclude-from=${ignore}`);
|
|
10560
|
-
tarArgs.push(".");
|
|
11148
|
+
async function attachRequestedDatabase(client, input) {
|
|
11149
|
+
if (input.choice === "none") return {
|
|
11150
|
+
line: chalk.yellow("none"),
|
|
11151
|
+
notes: ["No database attached (--db none). This project cannot deploy until one is:", " rebase cloud db create --type managed"],
|
|
11152
|
+
payload: {
|
|
11153
|
+
attached: false,
|
|
11154
|
+
type: null,
|
|
11155
|
+
reason: "requested_none"
|
|
11156
|
+
}
|
|
11157
|
+
};
|
|
10561
11158
|
try {
|
|
10562
|
-
await
|
|
11159
|
+
const row = await attachDatabaseRow(client, {
|
|
11160
|
+
projectId: input.projectId,
|
|
11161
|
+
type: input.choice,
|
|
11162
|
+
connectionString: input.connectionString
|
|
11163
|
+
});
|
|
11164
|
+
return {
|
|
11165
|
+
line: input.choice === "managed" ? `managed ${chalk.gray("· created at the first deploy")}` : `byodb ${chalk.gray("· not tested (`rebase cloud db test`)")}`,
|
|
11166
|
+
notes: [],
|
|
11167
|
+
payload: {
|
|
11168
|
+
attached: true,
|
|
11169
|
+
id: String(row.id),
|
|
11170
|
+
type: input.choice,
|
|
11171
|
+
materializedAt: input.choice === "managed" ? "first_deploy" : "now"
|
|
11172
|
+
}
|
|
11173
|
+
};
|
|
10563
11174
|
} catch (e) {
|
|
10564
|
-
|
|
11175
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
11176
|
+
return {
|
|
11177
|
+
line: chalk.red("not attached"),
|
|
11178
|
+
notes: [],
|
|
11179
|
+
warning: [`The project was created, but attaching its ${input.choice} database failed: ${message}`, `Finish it with: rebase cloud db create --type ${input.choice} --project ${input.projectRef}`],
|
|
11180
|
+
payload: {
|
|
11181
|
+
attached: false,
|
|
11182
|
+
type: input.choice,
|
|
11183
|
+
reason: "attach_failed",
|
|
11184
|
+
error: message
|
|
11185
|
+
}
|
|
11186
|
+
};
|
|
10565
11187
|
}
|
|
10566
|
-
return tarPath;
|
|
10567
11188
|
}
|
|
10568
11189
|
/**
|
|
10569
|
-
*
|
|
10570
|
-
*
|
|
10571
|
-
* Recorded on the deployment so a row in Deployment History says which
|
|
10572
|
-
* framework build shipped. Nothing else on the platform knows: an app that
|
|
10573
|
-
* links the framework locally pins it at package time, and a silent bump is
|
|
10574
|
-
* invisible afterwards — it has already cost one debugging session.
|
|
11190
|
+
* Which project `projects info` / `projects delete` acts on.
|
|
10575
11191
|
*
|
|
10576
|
-
*
|
|
10577
|
-
*
|
|
10578
|
-
*
|
|
10579
|
-
*
|
|
11192
|
+
* The id is optional — omitted, it falls back to `--project` or the link file —
|
|
11193
|
+
* and the dispatcher used to read it off `positionals()`, which skips only
|
|
11194
|
+
* LEADING `-` tokens and declares only the global cloud flags. So an undeclared
|
|
11195
|
+
* flag written after the action became the id: `rebase cloud projects delete
|
|
11196
|
+
* --force` looked up a project named "--force" and reported it missing, rather
|
|
11197
|
+
* than saying there is no such flag. Benign next to the deletes and writes the
|
|
11198
|
+
* rest of this family aimed at the wrong resource, but the same mistake, and
|
|
11199
|
+
* `positionals()` has no spec with which to do better — the handler's own
|
|
11200
|
+
* module does.
|
|
10580
11201
|
*
|
|
10581
|
-
*
|
|
10582
|
-
* recorded. Nothing about a deploy should fail over a bookkeeping string.
|
|
11202
|
+
* Exported so its tests drive the real parser rather than a copy of it.
|
|
10583
11203
|
*/
|
|
10584
|
-
function
|
|
10585
|
-
|
|
10586
|
-
|
|
10587
|
-
|
|
10588
|
-
|
|
10589
|
-
|
|
10590
|
-
|
|
10591
|
-
|
|
10592
|
-
|
|
10593
|
-
if (parent === dir) return void 0;
|
|
10594
|
-
dir = parent;
|
|
10595
|
-
}
|
|
11204
|
+
function resolveProjectArg(rawArgs, action) {
|
|
11205
|
+
const { positionals } = parseCloudArgs({
|
|
11206
|
+
spec: {},
|
|
11207
|
+
rawArgs,
|
|
11208
|
+
commandWords: 3,
|
|
11209
|
+
command: `cloud projects ${action}`,
|
|
11210
|
+
maxPositionals: 1
|
|
11211
|
+
});
|
|
11212
|
+
return positionals[0] || requireProjectRef(rawArgs);
|
|
10596
11213
|
}
|
|
10597
11214
|
/**
|
|
10598
|
-
* A
|
|
11215
|
+
* A project's database capacity, or null.
|
|
10599
11216
|
*
|
|
10600
|
-
*
|
|
10601
|
-
*
|
|
10602
|
-
*
|
|
10603
|
-
*
|
|
10604
|
-
* `warn`, which prints in every mode, to stderr. See `warn` in `context.ts`.
|
|
11217
|
+
* Swallows every failure on purpose. This decorates `status`; a control plane
|
|
11218
|
+
* that predates the `capacity` function 404s here, and an older CLI talking to a
|
|
11219
|
+
* newer one must still print the project. Losing the capacity line is a missing
|
|
11220
|
+
* nicety — failing the whole command over it would be the bug.
|
|
10605
11221
|
*/
|
|
10606
|
-
function
|
|
10607
|
-
|
|
11222
|
+
async function fetchCapacity(client, projectId) {
|
|
11223
|
+
try {
|
|
11224
|
+
return (await client.functions.invoke("capacity", void 0, {
|
|
11225
|
+
method: "GET",
|
|
11226
|
+
path: projectId
|
|
11227
|
+
}))?.database ?? null;
|
|
11228
|
+
} catch {
|
|
11229
|
+
return null;
|
|
11230
|
+
}
|
|
10608
11231
|
}
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
const bytes = fs.readFileSync(tarPath);
|
|
10612
|
-
const sizeMb = (bytes.length / 1024 / 1024).toFixed(1);
|
|
10613
|
-
if (bytes.length > MAX_SOURCE_UPLOAD_BYTES) fail(`Source context is ${sizeMb} MB — the upload cap is ${Math.round(MAX_SOURCE_UPLOAD_BYTES / 1024 / 1024)} MB.`, "Trim the build context: exclude sourcemaps (*.map), build output and large assets via .rebaseignore or .gitignore.");
|
|
10614
|
-
progress(chalk.gray(` Uploading source (${sizeMb} MB)...`));
|
|
10615
|
-
const res = await fetch(`${url}/api/functions/deploy/upload?projectId=${encodeURIComponent(projectId)}`, {
|
|
10616
|
-
method: "POST",
|
|
10617
|
-
headers: {
|
|
10618
|
-
Authorization: `Bearer ${token}`,
|
|
10619
|
-
"Content-Type": "application/gzip"
|
|
10620
|
-
},
|
|
10621
|
-
body: bytes
|
|
10622
|
-
});
|
|
10623
|
-
if (!res.ok) {
|
|
10624
|
-
const body = await res.text().catch(() => "");
|
|
10625
|
-
fail(`Source upload failed (${res.status}): ${body || res.statusText}`);
|
|
10626
|
-
}
|
|
10627
|
-
const data = await res.json();
|
|
10628
|
-
if (!data.source) fail("Upload endpoint did not return a source reference.");
|
|
10629
|
-
return data.source;
|
|
10630
|
-
}
|
|
10631
|
-
/**
|
|
10632
|
-
* Build, upload and deploy a project as a managed bundle.
|
|
10633
|
-
*
|
|
10634
|
-
* Builds the backend app into `dist-bundle` (unless one is pointed at with
|
|
10635
|
-
* `--bundle-dir`), packs it without `node_modules`, uploads it, and triggers a
|
|
10636
|
-
* deploy carrying the manifest so the control plane can validate intake fast.
|
|
10637
|
-
*/
|
|
10638
|
-
async function deployBundle(opts) {
|
|
10639
|
-
const projectRoot = requireProjectRoot();
|
|
10640
|
-
let bundleDir = opts.bundleDir ? path.resolve(process.cwd(), opts.bundleDir) : path.join(projectRoot, "dist-bundle");
|
|
10641
|
-
if (!opts.bundleDir) {
|
|
10642
|
-
const loaded = loadManifest(projectRoot);
|
|
10643
|
-
let target;
|
|
10644
|
-
try {
|
|
10645
|
-
target = selectDeployApp(loaded.manifest, opts.appName);
|
|
10646
|
-
} catch (err) {
|
|
10647
|
-
fail(err instanceof Error ? err.message : String(err));
|
|
10648
|
-
return;
|
|
10649
|
-
}
|
|
10650
|
-
if (target.app.type === "static") {
|
|
10651
|
-
progress(chalk.gray(` Building static app "${target.name}"...`));
|
|
10652
|
-
const staticDir = await buildAssetApp(projectRoot, target.name, target.app, loaded.manifest.rebase);
|
|
10653
|
-
if (!staticDir) {
|
|
10654
|
-
fail(`App "${target.name}" produced no bundle.`, "A static app needs both a `build` command and an `output` directory in rebase.json.");
|
|
10655
|
-
return;
|
|
10656
|
-
}
|
|
10657
|
-
bundleDir = staticDir;
|
|
10658
|
-
await uploadAndTrigger({
|
|
10659
|
-
...opts,
|
|
10660
|
-
bundleDir,
|
|
10661
|
-
appName: target.name
|
|
10662
|
-
});
|
|
10663
|
-
return;
|
|
10664
|
-
}
|
|
10665
|
-
progress(chalk.gray(" Building bundle..."));
|
|
10666
|
-
const backend = {
|
|
10667
|
-
name: target.name,
|
|
10668
|
-
app: target.app
|
|
10669
|
-
};
|
|
10670
|
-
const { graph: resourceGraph, issues: resourceIssues } = await deriveResourceGraph({ configDir: path.join(projectRoot, resolveBackendPaths(backend.app, projectRoot).config) });
|
|
10671
|
-
if (resourceIssues.length > 0) throw new Error(`${resourceIssues.length} problem(s) in the declared resources:\n` + resourceIssues.map((i) => ` ${i.path} ${i.message}`).join("\n"));
|
|
10672
|
-
bundleDir = (await buildBundle({
|
|
10673
|
-
projectRoot,
|
|
10674
|
-
appName: backend.name,
|
|
10675
|
-
app: backend.app,
|
|
10676
|
-
runtimeRange: loaded.manifest.rebase,
|
|
10677
|
-
resources: resourceGraph,
|
|
10678
|
-
skipTypeCheck: opts.skipTypeCheck,
|
|
10679
|
-
log: (m) => progress(chalk.gray(m))
|
|
10680
|
-
})).outDir;
|
|
10681
|
-
try {
|
|
10682
|
-
const folded = await foldFrontendIntoBundle({
|
|
10683
|
-
projectRoot,
|
|
10684
|
-
manifest: loaded.manifest,
|
|
10685
|
-
bundleDir,
|
|
10686
|
-
log: (m) => progress(m)
|
|
10687
|
-
});
|
|
10688
|
-
for (const outcome of folded) progress(chalk.gray(` folded ${outcome.appName} in (${outcome.fileCount} file(s), served at ${outcome.path})`));
|
|
10689
|
-
} catch (err) {
|
|
10690
|
-
fail(err instanceof Error ? err.message : String(err), "Fix the frontend build, or pass --no-static to deploy the API alone.");
|
|
10691
|
-
}
|
|
10692
|
-
}
|
|
10693
|
-
await uploadAndTrigger({
|
|
10694
|
-
...opts,
|
|
10695
|
-
bundleDir
|
|
10696
|
-
});
|
|
10697
|
-
}
|
|
10698
|
-
/**
|
|
10699
|
-
* Pack a built bundle, upload it, and trigger the deploy.
|
|
10700
|
-
*
|
|
10701
|
-
* Shared by every managed deploy, whichever kind of app produced the bundle: a
|
|
10702
|
-
* backend and a static app differ in what is built and in the `kind` their
|
|
10703
|
-
* manifest carries, and in nothing after that. Keeping one tail is what makes
|
|
10704
|
-
* that true rather than nearly true — the folding step was once missing from
|
|
10705
|
-
* one of two callers producing the same artifact, and the deploy shipped a
|
|
10706
|
-
* bundle with no site in it.
|
|
10707
|
-
*/
|
|
10708
|
-
async function uploadAndTrigger(opts) {
|
|
10709
|
-
const { client, url, projectId, projectRef, bundleDir } = opts;
|
|
10710
|
-
const manifest = readBundleManifest(bundleDir);
|
|
10711
|
-
if (manifest.hooks?.native) {
|
|
10712
|
-
const names = (manifest.hooks.nativeModules ?? []).map((m) => m.name).join(", ");
|
|
10713
|
-
fail(`This bundle depends on native modules${names ? ` (${names})` : ""}, which the managed runtime cannot run.`, "Remove the native dependency, or deploy on the custom runtime.");
|
|
10714
|
-
}
|
|
10715
|
-
const tarPath = path.join(os.tmpdir(), `rebase-bundle-${Date.now()}.tar.gz`);
|
|
10716
|
-
const token = client.auth.getSession()?.accessToken;
|
|
10717
|
-
if (!token) fail("Not authenticated.", "Run `rebase cloud login`.");
|
|
10718
|
-
let bundleId;
|
|
11232
|
+
async function projectInfo(rawArgs, projectRef) {
|
|
11233
|
+
const { client, url } = await requireClient(rawArgs);
|
|
10719
11234
|
try {
|
|
10720
|
-
await
|
|
10721
|
-
const
|
|
10722
|
-
|
|
10723
|
-
|
|
11235
|
+
const projectId = await resolveProjectRef(projectRef, client);
|
|
11236
|
+
const p = await client.data.collection("projects").findById(projectId);
|
|
11237
|
+
if (!p) fail(`Project ${projectRef} not found.`, void 0, "project_not_found");
|
|
11238
|
+
const [db, lastDeploy, baseDomain, capacity] = await Promise.all([
|
|
11239
|
+
firstRow(client, "databases", projectId),
|
|
11240
|
+
latestDeployment(client, projectId),
|
|
11241
|
+
fetchTenantBaseDomain(client, url),
|
|
11242
|
+
fetchCapacity(client, projectId)
|
|
11243
|
+
]);
|
|
11244
|
+
emit(() => {
|
|
11245
|
+
console.log("");
|
|
11246
|
+
console.log(` ${chalk.bold(p.name ?? "(unnamed)")} ${chalk.gray(`[${p.subdomain ?? p.id}]`)} ${colorStatus(p.status)}`);
|
|
11247
|
+
console.log("");
|
|
11248
|
+
keyValues([
|
|
11249
|
+
["Subdomain", projectHost(p, baseDomain)],
|
|
11250
|
+
["Custom domain", p.customDomain],
|
|
11251
|
+
["Repository", p.gitRepoUrl],
|
|
11252
|
+
["Branch", p.gitBranch],
|
|
11253
|
+
["Provider", p.provider],
|
|
11254
|
+
["Region", p.region],
|
|
11255
|
+
["Organization", p.organization !== void 0 ? String(p.organization) : void 0],
|
|
11256
|
+
["Database", db ? `${db.type} (${colorStatus(db.connectionStatus)})` : "none"],
|
|
11257
|
+
["Storage", capacity && capacity.limitMb > 0 ? `${capacity.usedMb} MB / ${capacity.limitMb} MB${capacity.usedFraction !== null ? ` (${(capacity.usedFraction * 100).toFixed(0)}%)` : ""}` : void 0],
|
|
11258
|
+
["Last deploy", lastDeploy ? `${colorStatus(lastDeploy.status)} · ${fmtDate(lastDeploy.createdAt)}` : "never"]
|
|
11259
|
+
]);
|
|
11260
|
+
if (capacity && capacity.state !== "ok") {
|
|
11261
|
+
console.log("");
|
|
11262
|
+
console.log(capacity.state === "locked" ? ` ${chalk.red.bold("✗ Database locked — over its storage limit")}` : ` ${chalk.yellow.bold("⚠ Database approaching its storage limit")}`);
|
|
11263
|
+
console.log(` ${chalk.gray(capacity.detail)}`);
|
|
11264
|
+
}
|
|
11265
|
+
console.log("");
|
|
11266
|
+
}, {
|
|
11267
|
+
id: String(p.id),
|
|
11268
|
+
name: p.name ?? null,
|
|
11269
|
+
slug: p.subdomain ?? null,
|
|
11270
|
+
host: projectHost(p, baseDomain) ?? null,
|
|
11271
|
+
customDomain: p.customDomain ?? null,
|
|
11272
|
+
repository: p.gitRepoUrl ?? null,
|
|
11273
|
+
branch: p.gitBranch ?? null,
|
|
11274
|
+
provider: p.provider ?? null,
|
|
11275
|
+
region: p.region ?? null,
|
|
11276
|
+
status: p.status ?? null,
|
|
11277
|
+
org: p.organization !== void 0 ? String(p.organization) : null,
|
|
11278
|
+
database: db ? {
|
|
11279
|
+
type: db.type ?? null,
|
|
11280
|
+
connectionStatus: db.connectionStatus ?? null,
|
|
11281
|
+
capacity: capacity ?? null
|
|
11282
|
+
} : null,
|
|
11283
|
+
lastDeploy: lastDeploy ? {
|
|
11284
|
+
id: String(lastDeploy.id),
|
|
11285
|
+
status: lastDeploy.status ?? null,
|
|
11286
|
+
createdAt: lastDeploy.createdAt ?? null
|
|
11287
|
+
} : null
|
|
11288
|
+
});
|
|
10724
11289
|
} catch (e) {
|
|
10725
|
-
|
|
10726
|
-
return;
|
|
10727
|
-
} finally {
|
|
10728
|
-
fs.rmSync(tarPath, { force: true });
|
|
10729
|
-
}
|
|
10730
|
-
if (!isJsonMode()) {
|
|
10731
|
-
console.log("");
|
|
10732
|
-
console.log(` 🚀 Triggering managed deployment for ${chalk.bold(projectRef)} (schema ${manifest.schemaVersion})...`);
|
|
11290
|
+
reportError(e, "Failed to load project");
|
|
10733
11291
|
}
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
11292
|
+
}
|
|
11293
|
+
async function deleteProject(rawArgs, projectRef) {
|
|
11294
|
+
const { flags: args } = parseCloudArgs({
|
|
11295
|
+
spec: {},
|
|
11296
|
+
rawArgs,
|
|
11297
|
+
commandWords: 3,
|
|
11298
|
+
command: "cloud projects delete",
|
|
11299
|
+
maxPositionals: 1
|
|
11300
|
+
});
|
|
11301
|
+
const { client } = await requireClient(rawArgs);
|
|
11302
|
+
const projectId = await resolveProjectRef(projectRef, client);
|
|
11303
|
+
const p = await client.data.collection("projects").findById(projectId).catch(() => void 0);
|
|
11304
|
+
if (!p) fail(`Project ${projectRef} not found.`, void 0, "project_not_found");
|
|
11305
|
+
await confirmDestructive({
|
|
11306
|
+
yes: Boolean(args["--yes"]),
|
|
11307
|
+
prompt: `Permanently delete project "${p.name ?? projectRef}" (${p.subdomain ?? projectRef})? This tears down its deployment.`
|
|
10745
11308
|
});
|
|
10746
11309
|
try {
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
11310
|
+
await client.data.collection("projects").delete(projectId);
|
|
11311
|
+
success(`Deleted project ${chalk.bold(p.name ?? projectId)}`);
|
|
11312
|
+
emit(() => {}, {
|
|
10750
11313
|
success: true,
|
|
10751
|
-
|
|
10752
|
-
|
|
11314
|
+
id: projectId,
|
|
11315
|
+
name: p.name ?? null,
|
|
11316
|
+
slug: p.subdomain ?? null
|
|
10753
11317
|
});
|
|
10754
|
-
else {
|
|
10755
|
-
console.log(chalk.green(` ✓ Managed deploy started (deployment ${res.deployment.id}).`));
|
|
10756
|
-
console.log(chalk.gray(" Track it with `rebase cloud logs` or in the console."));
|
|
10757
|
-
}
|
|
10758
11318
|
} catch (e) {
|
|
10759
|
-
reportError(e, "
|
|
11319
|
+
reportError(e, "Failed to delete project");
|
|
10760
11320
|
}
|
|
10761
11321
|
}
|
|
10762
|
-
function
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
}
|
|
11322
|
+
async function firstRow(client, collection, projectId) {
|
|
11323
|
+
return (await client.data.collection(collection).find({
|
|
11324
|
+
where: { project: ["==", projectId] },
|
|
11325
|
+
limit: 1
|
|
11326
|
+
})).data[0];
|
|
10767
11327
|
}
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
if (
|
|
10777
|
-
|
|
10778
|
-
|
|
10779
|
-
if (hours < 24) return `${hours}h ago`;
|
|
10780
|
-
return `${Math.floor(hours / 24)}d ago`;
|
|
11328
|
+
async function latestDeployment(client, projectId) {
|
|
11329
|
+
return (await client.data.collection("deployments").find({
|
|
11330
|
+
where: { project: ["==", projectId] },
|
|
11331
|
+
orderBy: ["createdAt", "desc"],
|
|
11332
|
+
limit: 1
|
|
11333
|
+
})).data[0];
|
|
11334
|
+
}
|
|
11335
|
+
function fmtDate(value) {
|
|
11336
|
+
if (!value) return "—";
|
|
11337
|
+
const d = new Date(value);
|
|
11338
|
+
return isNaN(d.getTime()) ? value : d.toLocaleString();
|
|
10781
11339
|
}
|
|
11340
|
+
//#endregion
|
|
11341
|
+
//#region src/commands/cloud/bundle-deploy.ts
|
|
10782
11342
|
/**
|
|
10783
|
-
*
|
|
11343
|
+
* Deploying a project as a managed **bundle** rather than a source build.
|
|
10784
11344
|
*
|
|
10785
|
-
* `
|
|
10786
|
-
*
|
|
10787
|
-
*
|
|
10788
|
-
*
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
10797
|
-
|
|
10798
|
-
const
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
lines: [`Building from git: ${repo}${branch ? ` (${branch})` : ""}.`]
|
|
10806
|
-
};
|
|
10807
|
-
}
|
|
10808
|
-
if (pick(deploymentRow, "sourceRef", "source_ref")) {
|
|
10809
|
-
const age = timeAgo(latest?.createdAt ?? latest?.created_at, now);
|
|
10810
|
-
return {
|
|
10811
|
-
managed,
|
|
10812
|
-
source: "snapshot",
|
|
10813
|
-
lines: [`Rebuilding the stored source archive${latest?.id !== void 0 ? ` from deployment ${latest.id}` : ""}${age ? `, uploaded ${age}` : ""}.`, "This directory is NOT uploaded — pass `--source .` to build what is on disk."]
|
|
10814
|
-
};
|
|
11345
|
+
* `rebase cloud deploy --bundle` builds the bundle, tars it, uploads it to the
|
|
11346
|
+
* control plane's bundle endpoint, and triggers a deploy carrying the bundle id
|
|
11347
|
+
* and its generated manifest. The control plane resolves a runtime from the
|
|
11348
|
+
* manifest's range and runs the platform image with this bundle — the managed
|
|
11349
|
+
* path. A project not in managed mode, or one whose bundle fails intake, is told
|
|
11350
|
+
* so by the control plane; this side just packages and hands it over.
|
|
11351
|
+
*
|
|
11352
|
+
* The pieces here are separated from the network calls so they can be tested: the
|
|
11353
|
+
* manifest read, the tar packaging, and the request body assembly are pure enough
|
|
11354
|
+
* to check without a control plane.
|
|
11355
|
+
*/
|
|
11356
|
+
/** Read and shallow-validate a built bundle's manifest. */
|
|
11357
|
+
function readBundleManifest(bundleDir) {
|
|
11358
|
+
const manifestPath = path.join(bundleDir, "manifest.json");
|
|
11359
|
+
if (!fs.existsSync(manifestPath)) throw new Error(`No manifest.json in ${bundleDir}. Run \`rebase build\` first.`);
|
|
11360
|
+
let manifest;
|
|
11361
|
+
try {
|
|
11362
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
11363
|
+
} catch (err) {
|
|
11364
|
+
throw new Error(`${manifestPath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
10815
11365
|
}
|
|
10816
|
-
|
|
10817
|
-
|
|
10818
|
-
source: "none",
|
|
10819
|
-
lines: ["This project has no git repository configured and no stored source archive to rebuild.", "Upload this directory with `--source .`, or set a repository URL in the project settings."]
|
|
10820
|
-
};
|
|
10821
|
-
}
|
|
10822
|
-
/** `code` of the warning below, and the field name it sets in the payload. */
|
|
10823
|
-
var EJECTS_MANAGED_RUNTIME = "ejects_managed_runtime";
|
|
10824
|
-
/** The one sentence that says a source build undoes `runtimeMode: managed`. */
|
|
10825
|
-
function ejectWarning(projectRef) {
|
|
10826
|
-
return {
|
|
10827
|
-
code: EJECTS_MANAGED_RUNTIME,
|
|
10828
|
-
message: `${projectRef} runs on the managed runtime — this build ejects it to a custom container.`,
|
|
10829
|
-
hint: "Use `rebase cloud deploy --bundle` to stay on managed."
|
|
10830
|
-
};
|
|
11366
|
+
if (typeof manifest.bundleFormat !== "number" || !manifest.runtime?.range) throw new Error(`${manifestPath} is not a valid bundle manifest.`);
|
|
11367
|
+
return manifest;
|
|
10831
11368
|
}
|
|
10832
11369
|
/**
|
|
10833
|
-
*
|
|
10834
|
-
* to let it through.
|
|
10835
|
-
*
|
|
10836
|
-
* Every path below this point builds a container image, and a successful one
|
|
10837
|
-
* sets `runtimeMode: "custom"` server-side. So the question is never "which flag
|
|
10838
|
-
* was used" but "did the caller ask to leave the managed runtime", and only
|
|
10839
|
-
* `--force` answers it.
|
|
11370
|
+
* Tar a built bundle into a gzipped archive.
|
|
10840
11371
|
*
|
|
10841
|
-
*
|
|
10842
|
-
*
|
|
10843
|
-
*
|
|
10844
|
-
*
|
|
10845
|
-
* answer. That is exactly how a live project got flipped to `custom` by someone
|
|
10846
|
-
* whose actual intent was "deploy what I have here", and it is the same
|
|
10847
|
-
* ignorance the bare form is refused for. Same ignorance, same refusal.
|
|
11372
|
+
* `node_modules` is excluded on purpose: the bundle ships a `package.json`, and
|
|
11373
|
+
* the managed runtime installs the declared dependencies at boot. Uploading an
|
|
11374
|
+
* installed `node_modules` would bloat the archive and could carry a
|
|
11375
|
+
* platform-specific build that will not run on the runtime image.
|
|
10848
11376
|
*/
|
|
10849
|
-
function
|
|
10850
|
-
|
|
10851
|
-
|
|
10852
|
-
|
|
10853
|
-
|
|
10854
|
-
|
|
10855
|
-
|
|
10856
|
-
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
|
|
10860
|
-
|
|
10861
|
-
|
|
11377
|
+
function packBundle(bundleDir, outPath) {
|
|
11378
|
+
return new Promise((resolve, reject) => {
|
|
11379
|
+
const child = spawn("tar", [
|
|
11380
|
+
"-czf",
|
|
11381
|
+
outPath,
|
|
11382
|
+
"--no-xattrs",
|
|
11383
|
+
"--exclude",
|
|
11384
|
+
"node_modules",
|
|
11385
|
+
"-C",
|
|
11386
|
+
bundleDir,
|
|
11387
|
+
"."
|
|
11388
|
+
], {
|
|
11389
|
+
stdio: "inherit",
|
|
11390
|
+
env: {
|
|
11391
|
+
...process.env,
|
|
11392
|
+
COPYFILE_DISABLE: "1"
|
|
11393
|
+
}
|
|
11394
|
+
});
|
|
11395
|
+
child.on("error", reject);
|
|
11396
|
+
child.on("close", (code) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`tar exited ${code}`)));
|
|
11397
|
+
});
|
|
10862
11398
|
}
|
|
10863
11399
|
/**
|
|
10864
|
-
*
|
|
10865
|
-
*
|
|
10866
|
-
* Pure, and separate from the printing, because the printing is what went
|
|
10867
|
-
* wrong: the eject warning used to be written inline behind `!isJsonMode()`, so
|
|
10868
|
-
* the fact that a deploy ejects a managed project existed only as a side effect
|
|
10869
|
-
* of a TTY being attached. Deciding here, emitting once at the call site, means
|
|
10870
|
-
* the decision cannot be output-mode-dependent again.
|
|
11400
|
+
* Assemble the deploy-trigger body for a bundle deploy.
|
|
10871
11401
|
*
|
|
10872
|
-
* The
|
|
10873
|
-
*
|
|
10874
|
-
*
|
|
10875
|
-
* knows — the warning is for the transcript and the payload, which is what
|
|
10876
|
-
* anyone reviewing the deploy afterwards actually reads.
|
|
11402
|
+
* The manifest travels with the trigger so the control plane can validate intake
|
|
11403
|
+
* without unpacking the uploaded archive first — a rejection (native deps, no
|
|
11404
|
+
* matching runtime) is then a fast, cheap answer.
|
|
10877
11405
|
*/
|
|
10878
|
-
function
|
|
10879
|
-
return opts.managed ? [ejectWarning(projectRef)] : [];
|
|
10880
|
-
}
|
|
10881
|
-
/** The warning half of a deploy's JSON payload — merged into whatever it emits. */
|
|
10882
|
-
function warningPayload(warnings) {
|
|
11406
|
+
function bundleDeployBody(input) {
|
|
10883
11407
|
return {
|
|
10884
|
-
|
|
10885
|
-
|
|
10886
|
-
|
|
10887
|
-
|
|
10888
|
-
|
|
10889
|
-
|
|
11408
|
+
projectId: input.projectId,
|
|
11409
|
+
bundleId: input.bundleId,
|
|
11410
|
+
bundleManifest: input.manifest,
|
|
11411
|
+
app: input.app ?? input.manifest.app ?? "backend",
|
|
11412
|
+
client: "cli",
|
|
11413
|
+
frameworkVersion: input.manifest.runtime?.builtAgainst,
|
|
11414
|
+
...input.declaredApps?.length ? { declaredApps: input.declaredApps } : {},
|
|
11415
|
+
...input.message ? { message: input.message } : {}
|
|
10890
11416
|
};
|
|
10891
11417
|
}
|
|
10892
11418
|
/**
|
|
10893
|
-
*
|
|
11419
|
+
* The apps a project manifest declares.
|
|
10894
11420
|
*
|
|
10895
|
-
*
|
|
10896
|
-
*
|
|
10897
|
-
*
|
|
10898
|
-
*
|
|
11421
|
+
* A deploy only ever ships ONE app's bundle, so the trigger alone could never
|
|
11422
|
+
* tell the platform that the repository also contains a web frontend and an
|
|
11423
|
+
* admin panel — and the Apps page, whose whole job is to show the set, listed a
|
|
11424
|
+
* single entry called "backend". Sending the declared set fixes that without
|
|
11425
|
+
* pretending the others are deployed: the platform registers them, and their
|
|
11426
|
+
* status says what is actually true.
|
|
10899
11427
|
*/
|
|
10900
|
-
|
|
10901
|
-
|
|
10902
|
-
|
|
10903
|
-
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
10907
|
-
} catch {
|
|
10908
|
-
return {};
|
|
10909
|
-
}
|
|
11428
|
+
function declaredAppsFrom(manifest) {
|
|
11429
|
+
const apps = manifest?.apps;
|
|
11430
|
+
if (!apps || typeof apps !== "object") return [];
|
|
11431
|
+
return Object.entries(apps).filter(([name]) => name.trim().length > 0).map(([name, value]) => ({
|
|
11432
|
+
name,
|
|
11433
|
+
type: value?.type === "backend" ? "backend" : "static"
|
|
11434
|
+
}));
|
|
10910
11435
|
}
|
|
10911
|
-
/**
|
|
10912
|
-
|
|
10913
|
-
|
|
10914
|
-
|
|
10915
|
-
|
|
10916
|
-
|
|
10917
|
-
|
|
10918
|
-
|
|
10919
|
-
|
|
10920
|
-
|
|
10921
|
-
|
|
10922
|
-
|
|
10923
|
-
const
|
|
10924
|
-
|
|
10925
|
-
const app = manifest.apps[appName];
|
|
10926
|
-
if (app?.type === "static") return true;
|
|
10927
|
-
if (app?.type === "backend") return app.runtime === "managed";
|
|
10928
|
-
}
|
|
10929
|
-
const backend = findBackendApp(manifest);
|
|
10930
|
-
if (!backend) return Object.keys(manifest.apps).length > 0;
|
|
10931
|
-
return backend.app.runtime === "managed";
|
|
10932
|
-
} catch {
|
|
10933
|
-
return false;
|
|
11436
|
+
/** Upload a bundle archive; returns the control-plane bundle id. */
|
|
11437
|
+
async function uploadBundle(url, token, projectId, tarPath) {
|
|
11438
|
+
const bytes = fs.readFileSync(tarPath);
|
|
11439
|
+
const res = await fetch(`${url}/api/functions/deploy/bundle/upload?projectId=${encodeURIComponent(projectId)}`, {
|
|
11440
|
+
method: "POST",
|
|
11441
|
+
headers: {
|
|
11442
|
+
Authorization: `Bearer ${token}`,
|
|
11443
|
+
"Content-Type": "application/gzip"
|
|
11444
|
+
},
|
|
11445
|
+
body: bytes
|
|
11446
|
+
});
|
|
11447
|
+
if (!res.ok) {
|
|
11448
|
+
const body = await res.text().catch(() => "");
|
|
11449
|
+
throw new Error(`Bundle upload failed (${res.status}): ${body || res.statusText}`);
|
|
10934
11450
|
}
|
|
11451
|
+
const data = await res.json();
|
|
11452
|
+
if (!data.bundleId) throw new Error("Bundle upload endpoint did not return a bundle id.");
|
|
11453
|
+
return data.bundleId;
|
|
10935
11454
|
}
|
|
11455
|
+
//#endregion
|
|
11456
|
+
//#region src/commands/cloud/deploy.ts
|
|
10936
11457
|
/**
|
|
10937
|
-
* `rebase cloud deploy
|
|
10938
|
-
* line named.
|
|
10939
|
-
*
|
|
10940
|
-
* Parsed through `parseCloudArgs` rather than `arg` directly, and the reason is
|
|
10941
|
-
* the positional. This command used to parse `rawArgs.slice(2)` permissively
|
|
10942
|
-
* and read `_[0]` as the app name — but `rawArgs` is the WHOLE `process.argv`,
|
|
10943
|
-
* so `_` opens with the command words themselves. `_[0]` was therefore the
|
|
10944
|
-
* literal string `"cloud"` on every run, which then went to `selectDeployApp`
|
|
10945
|
-
* and came back as:
|
|
10946
|
-
*
|
|
10947
|
-
* This repository declares no app named "cloud". It declares: backend, web.
|
|
11458
|
+
* `rebase cloud deploy` and `rebase cloud logs`.
|
|
10948
11459
|
*
|
|
10949
|
-
*
|
|
10950
|
-
*
|
|
10951
|
-
*
|
|
10952
|
-
* as a fault in their `rebase.json` rather than in the CLI's own parse.
|
|
11460
|
+
* `deploy` triggers the control-plane `deploy` function, then tails the build
|
|
11461
|
+
* logs from the deployment record until it succeeds or fails. `logs` shows the
|
|
11462
|
+
* latest build log, or runtime logs with `--runtime`.
|
|
10953
11463
|
*
|
|
10954
|
-
*
|
|
10955
|
-
*
|
|
10956
|
-
*
|
|
11464
|
+
* There are three deploys behind the one verb, and which one runs depends on the
|
|
11465
|
+
* flags: `--bundle` builds and uploads a managed bundle, `--source .` uploads
|
|
11466
|
+
* this directory as a build context, and the bare form uploads nothing and asks
|
|
11467
|
+
* the control plane to rebuild what it already holds. That last one is the
|
|
11468
|
+
* dangerous one — see `planBareDeploy`.
|
|
10957
11469
|
*/
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
|
|
10961
|
-
|
|
10962
|
-
|
|
10963
|
-
"--message": String,
|
|
10964
|
-
"--bundle": Boolean,
|
|
10965
|
-
"--bundle-dir": String,
|
|
10966
|
-
"--skip-type-check": Boolean,
|
|
10967
|
-
"--force": Boolean,
|
|
10968
|
-
"-m": "--message"
|
|
10969
|
-
},
|
|
10970
|
-
rawArgs,
|
|
10971
|
-
commandWords: 2,
|
|
10972
|
-
command: "cloud deploy",
|
|
10973
|
-
maxPositionals: 1
|
|
10974
|
-
});
|
|
10975
|
-
return {
|
|
10976
|
-
flags,
|
|
10977
|
-
appName: positionals[0]
|
|
10978
|
-
};
|
|
11470
|
+
var POLL_INTERVAL_MS = 1500;
|
|
11471
|
+
var POLL_TIMEOUT_MS = 900 * 1e3;
|
|
11472
|
+
var MAX_SOURCE_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
11473
|
+
function sleep(ms) {
|
|
11474
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
10979
11475
|
}
|
|
10980
|
-
|
|
10981
|
-
|
|
10982
|
-
|
|
10983
|
-
|
|
10984
|
-
|
|
10985
|
-
|
|
10986
|
-
|
|
10987
|
-
|
|
10988
|
-
|
|
10989
|
-
|
|
10990
|
-
|
|
10991
|
-
|
|
10992
|
-
|
|
10993
|
-
bundleDir: args["--bundle-dir"],
|
|
10994
|
-
message: args["--message"],
|
|
10995
|
-
appName,
|
|
10996
|
-
skipTypeCheck: args["--skip-type-check"] === true
|
|
11476
|
+
function run(cmd, cmdArgs, cwd, env) {
|
|
11477
|
+
return new Promise((resolve, reject) => {
|
|
11478
|
+
const child = spawn(cmd, cmdArgs, {
|
|
11479
|
+
cwd,
|
|
11480
|
+
env: env ? {
|
|
11481
|
+
...process.env,
|
|
11482
|
+
...env
|
|
11483
|
+
} : void 0,
|
|
11484
|
+
stdio: [
|
|
11485
|
+
"ignore",
|
|
11486
|
+
"ignore",
|
|
11487
|
+
"pipe"
|
|
11488
|
+
]
|
|
10997
11489
|
});
|
|
10998
|
-
|
|
10999
|
-
|
|
11000
|
-
|
|
11001
|
-
|
|
11002
|
-
|
|
11003
|
-
|
|
11004
|
-
|
|
11005
|
-
|
|
11006
|
-
|
|
11007
|
-
|
|
11008
|
-
|
|
11009
|
-
const
|
|
11010
|
-
|
|
11011
|
-
|
|
11012
|
-
|
|
11013
|
-
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
|
|
11017
|
-
|
|
11018
|
-
|
|
11019
|
-
|
|
11020
|
-
if (!token) fail("Not authenticated.", "Run `rebase cloud login`.");
|
|
11021
|
-
source = await uploadSource(url, token, projectId, tarPath);
|
|
11022
|
-
} finally {
|
|
11023
|
-
fs.rmSync(tarPath, { force: true });
|
|
11024
|
-
}
|
|
11025
|
-
}
|
|
11026
|
-
if (!isJsonMode()) {
|
|
11027
|
-
console.log("");
|
|
11028
|
-
console.log(` 🚀 Triggering deployment for project ${chalk.bold(projectRef)}${source ? " from uploaded source" : ""}...`);
|
|
11029
|
-
}
|
|
11030
|
-
const body = { projectId };
|
|
11031
|
-
if (source) body.source = source;
|
|
11032
|
-
if (args["--message"]) body.message = args["--message"];
|
|
11033
|
-
body.client = "cli";
|
|
11034
|
-
const frameworkVersion = resolveFrameworkVersion(args["--source"] ?? process.cwd());
|
|
11035
|
-
if (frameworkVersion) body.frameworkVersion = frameworkVersion;
|
|
11036
|
-
let triggered;
|
|
11490
|
+
let stderr = "";
|
|
11491
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
11492
|
+
child.on("error", reject);
|
|
11493
|
+
child.on("close", (code) => code === 0 ? resolve() : reject(new Error(stderr || `${cmd} exited ${code}`)));
|
|
11494
|
+
});
|
|
11495
|
+
}
|
|
11496
|
+
/**
|
|
11497
|
+
* Package `sourceDir` into a gzipped tarball, honoring `.gitignore`/`.rebaseignore`
|
|
11498
|
+
* and always excluding `.git` and `node_modules`. Returns the temp archive path.
|
|
11499
|
+
*/
|
|
11500
|
+
async function createSourceTarball(sourceDir) {
|
|
11501
|
+
const dir = path.resolve(sourceDir);
|
|
11502
|
+
if (!fs.existsSync(dir)) fail(`Source directory not found: ${dir}`);
|
|
11503
|
+
const tarPath = path.join(os.tmpdir(), `rebase-src-${Date.now()}.tar.gz`);
|
|
11504
|
+
const tarArgs = [
|
|
11505
|
+
"-czf",
|
|
11506
|
+
tarPath,
|
|
11507
|
+
"--exclude=.git",
|
|
11508
|
+
"--exclude=node_modules"
|
|
11509
|
+
];
|
|
11510
|
+
for (const ignore of [".gitignore", ".rebaseignore"]) if (fs.existsSync(path.join(dir, ignore))) tarArgs.push(`--exclude-from=${ignore}`);
|
|
11511
|
+
tarArgs.push(".");
|
|
11037
11512
|
try {
|
|
11038
|
-
|
|
11039
|
-
if (!res?.deployment?.id) fail("Control plane did not return a deployment id.");
|
|
11040
|
-
triggered = {
|
|
11041
|
-
deploymentId: String(res.deployment.id),
|
|
11042
|
-
deduplicated: res.deduplicated === true
|
|
11043
|
-
};
|
|
11513
|
+
await run("tar", tarArgs, dir, { COPYFILE_DISABLE: "1" });
|
|
11044
11514
|
} catch (e) {
|
|
11045
|
-
|
|
11046
|
-
}
|
|
11047
|
-
const { deploymentId, deduplicated } = triggered;
|
|
11048
|
-
if (!isJsonMode()) console.log(chalk.gray(deduplicated ? ` Deployment ${deploymentId} is already running — following it.` : ` Deployment ${deploymentId} created.${frameworkVersion ? ` (@rebasepro/* ${frameworkVersion})` : ""}`));
|
|
11049
|
-
if (args["--no-follow"]) {
|
|
11050
|
-
emit(() => {
|
|
11051
|
-
console.log(chalk.gray(" Not following logs (--no-follow). Check status with `rebase cloud logs`."));
|
|
11052
|
-
console.log("");
|
|
11053
|
-
}, {
|
|
11054
|
-
deploymentId,
|
|
11055
|
-
deduplicated,
|
|
11056
|
-
frameworkVersion: frameworkVersion ?? null,
|
|
11057
|
-
following: false,
|
|
11058
|
-
...warningPayload(warnings)
|
|
11059
|
-
});
|
|
11060
|
-
return;
|
|
11061
|
-
}
|
|
11062
|
-
if (!isJsonMode()) {
|
|
11063
|
-
console.log(chalk.gray(" Streaming build logs (Ctrl-C to stop watching — the build keeps running):"));
|
|
11064
|
-
console.log("");
|
|
11515
|
+
fail(`Failed to package source: ${e instanceof Error ? e.message : String(e)}`);
|
|
11065
11516
|
}
|
|
11066
|
-
|
|
11067
|
-
emit(() => {}, {
|
|
11068
|
-
deploymentId,
|
|
11069
|
-
deduplicated,
|
|
11070
|
-
frameworkVersion: frameworkVersion ?? null,
|
|
11071
|
-
following: true,
|
|
11072
|
-
status,
|
|
11073
|
-
...warningPayload(warnings)
|
|
11074
|
-
});
|
|
11517
|
+
return tarPath;
|
|
11075
11518
|
}
|
|
11076
11519
|
/**
|
|
11077
|
-
*
|
|
11520
|
+
* The `@rebasepro/*` version this source directory actually resolves.
|
|
11078
11521
|
*
|
|
11079
|
-
*
|
|
11080
|
-
*
|
|
11081
|
-
*
|
|
11082
|
-
*
|
|
11083
|
-
* deployment this very command had just created. With no id in the message the
|
|
11084
|
-
* only available reading was "someone else is deploying, back off", and the
|
|
11085
|
-
* build stream was lost either way.
|
|
11522
|
+
* Recorded on the deployment so a row in Deployment History says which
|
|
11523
|
+
* framework build shipped. Nothing else on the platform knows: an app that
|
|
11524
|
+
* links the framework locally pins it at package time, and a silent bump is
|
|
11525
|
+
* invisible afterwards — it has already cost one debugging session.
|
|
11086
11526
|
*
|
|
11087
|
-
*
|
|
11088
|
-
*
|
|
11089
|
-
*
|
|
11527
|
+
* `@rebasepro/server` first, because that is what the deployed backend runs;
|
|
11528
|
+
* `@rebasepro/client` is the fallback for a frontend-only bundle. Resolution is
|
|
11529
|
+
* a plain walk up from the source directory rather than `require.resolve`,
|
|
11530
|
+
* which would answer for the CLI's own install tree instead of the app's.
|
|
11531
|
+
*
|
|
11532
|
+
* Best effort by construction: a version that cannot be read is simply not
|
|
11533
|
+
* recorded. Nothing about a deploy should fail over a bookkeeping string.
|
|
11090
11534
|
*/
|
|
11091
|
-
function
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
const
|
|
11095
|
-
|
|
11096
|
-
|
|
11097
|
-
|
|
11098
|
-
}
|
|
11099
|
-
|
|
11535
|
+
function resolveFrameworkVersion(sourceDir) {
|
|
11536
|
+
let dir = path.resolve(sourceDir);
|
|
11537
|
+
for (;;) {
|
|
11538
|
+
for (const pkg of ["@rebasepro/server", "@rebasepro/client"]) try {
|
|
11539
|
+
const manifest = path.join(dir, "node_modules", ...pkg.split("/"), "package.json");
|
|
11540
|
+
const version = JSON.parse(fs.readFileSync(manifest, "utf8")).version;
|
|
11541
|
+
if (typeof version === "string" && version.trim() !== "") return version.trim();
|
|
11542
|
+
} catch {}
|
|
11543
|
+
const parent = path.dirname(dir);
|
|
11544
|
+
if (parent === dir) return void 0;
|
|
11545
|
+
dir = parent;
|
|
11100
11546
|
}
|
|
11101
|
-
if (err?.status === 402) fail(err.message || "Payment required before deploying.", "Attach a card once with `rebase cloud billing setup`, then deploy again.", "payment_required");
|
|
11102
|
-
if (err?.status === 400 && err.details?.intakeCode) fail(err.message || "This bundle was refused.", err.details.hint ?? "Run `rebase cloud resources` to see what this project is given.", err.details.intakeCode);
|
|
11103
|
-
reportError(e, "Failed to trigger deployment");
|
|
11104
11547
|
}
|
|
11105
11548
|
/**
|
|
11106
|
-
*
|
|
11107
|
-
* terminal status; a non-success still exits non-zero, as it always has.
|
|
11549
|
+
* A progress line for a human — dropped entirely in JSON mode.
|
|
11108
11550
|
*
|
|
11109
|
-
*
|
|
11110
|
-
*
|
|
11551
|
+
* Progress is not a result. In JSON mode stdout carries the one result value
|
|
11552
|
+
* and nothing else, so every unguarded `console.log` on a deploy path was a
|
|
11553
|
+
* line printed in front of the JSON, breaking the parser meant to read it.
|
|
11554
|
+
* Warnings are the other half of this rule and go the other way: they are
|
|
11555
|
+
* `warn`, which prints in every mode, to stderr. See `warn` in `context.ts`.
|
|
11111
11556
|
*/
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
11117
|
-
|
|
11557
|
+
function progress(line) {
|
|
11558
|
+
if (!isJsonMode()) console.log(line);
|
|
11559
|
+
}
|
|
11560
|
+
/** Upload a build-context tarball; returns the opaque `source` ref for deploy. */
|
|
11561
|
+
async function uploadSource(url, token, projectId, tarPath) {
|
|
11562
|
+
const bytes = fs.readFileSync(tarPath);
|
|
11563
|
+
const sizeMb = (bytes.length / 1024 / 1024).toFixed(1);
|
|
11564
|
+
if (bytes.length > MAX_SOURCE_UPLOAD_BYTES) fail(`Source context is ${sizeMb} MB — the upload cap is ${Math.round(MAX_SOURCE_UPLOAD_BYTES / 1024 / 1024)} MB.`, "Trim the build context: exclude sourcemaps (*.map), build output and large assets via .rebaseignore or .gitignore.");
|
|
11565
|
+
progress(chalk.gray(` Uploading source (${sizeMb} MB)...`));
|
|
11566
|
+
const res = await fetch(`${url}/api/functions/deploy/upload?projectId=${encodeURIComponent(projectId)}`, {
|
|
11567
|
+
method: "POST",
|
|
11568
|
+
headers: {
|
|
11569
|
+
Authorization: `Bearer ${token}`,
|
|
11570
|
+
"Content-Type": "application/gzip"
|
|
11571
|
+
},
|
|
11572
|
+
body: bytes
|
|
11573
|
+
});
|
|
11574
|
+
if (!res.ok) {
|
|
11575
|
+
const body = await res.text().catch(() => "");
|
|
11576
|
+
fail(`Source upload failed (${res.status}): ${body || res.statusText}`);
|
|
11577
|
+
}
|
|
11578
|
+
const data = await res.json();
|
|
11579
|
+
if (!data.source) fail("Upload endpoint did not return a source reference.");
|
|
11580
|
+
return data.source;
|
|
11581
|
+
}
|
|
11582
|
+
/**
|
|
11583
|
+
* Build, upload and deploy a project as a managed bundle.
|
|
11584
|
+
*
|
|
11585
|
+
* Builds the backend app into `dist-bundle` (unless one is pointed at with
|
|
11586
|
+
* `--bundle-dir`), packs it without `node_modules`, uploads it, and triggers a
|
|
11587
|
+
* deploy carrying the manifest so the control plane can validate intake fast.
|
|
11588
|
+
*/
|
|
11589
|
+
async function deployBundle(opts) {
|
|
11590
|
+
const projectRoot = requireProjectRoot();
|
|
11591
|
+
let bundleDir = opts.bundleDir ? path.resolve(process.cwd(), opts.bundleDir) : path.join(projectRoot, "dist-bundle");
|
|
11592
|
+
if (!opts.bundleDir) {
|
|
11593
|
+
const loaded = loadManifest(projectRoot);
|
|
11594
|
+
let target;
|
|
11118
11595
|
try {
|
|
11119
|
-
|
|
11120
|
-
} catch (
|
|
11121
|
-
|
|
11596
|
+
target = selectDeployApp(loaded.manifest, opts.appName);
|
|
11597
|
+
} catch (err) {
|
|
11598
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
11599
|
+
return;
|
|
11122
11600
|
}
|
|
11123
|
-
if (
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
|
|
11128
|
-
|
|
11129
|
-
if (quiet) {
|
|
11130
|
-
printJson({ error: {
|
|
11131
|
-
message: `Deployment ${deploymentId} ${dep.status}.`,
|
|
11132
|
-
code: "deploy_failed",
|
|
11133
|
-
status: null,
|
|
11134
|
-
deploymentId,
|
|
11135
|
-
logs
|
|
11136
|
-
} });
|
|
11137
|
-
process.exit(1);
|
|
11138
|
-
}
|
|
11139
|
-
console.log("");
|
|
11140
|
-
console.log(chalk.bold.red(` ✗ Deployment ${dep.status}`));
|
|
11141
|
-
console.log("");
|
|
11142
|
-
process.exit(1);
|
|
11143
|
-
}
|
|
11144
|
-
if (!quiet) {
|
|
11145
|
-
console.log("");
|
|
11146
|
-
console.log(chalk.bold.green(" ✓ Deployment succeeded"));
|
|
11147
|
-
console.log("");
|
|
11601
|
+
if (target.app.type === "static") {
|
|
11602
|
+
progress(chalk.gray(` Building static app "${target.name}"...`));
|
|
11603
|
+
const staticDir = await buildAssetApp(projectRoot, target.name, target.app, loaded.manifest.rebase);
|
|
11604
|
+
if (!staticDir) {
|
|
11605
|
+
fail(`App "${target.name}" produced no bundle.`, "A static app needs both a `build` command and an `output` directory in rebase.json.");
|
|
11606
|
+
return;
|
|
11148
11607
|
}
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11608
|
+
bundleDir = staticDir;
|
|
11609
|
+
await uploadAndTrigger({
|
|
11610
|
+
...opts,
|
|
11611
|
+
bundleDir,
|
|
11612
|
+
appName: target.name
|
|
11613
|
+
});
|
|
11614
|
+
return;
|
|
11154
11615
|
}
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
"
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
|
|
11169
|
-
|
|
11616
|
+
progress(chalk.gray(" Building bundle..."));
|
|
11617
|
+
const backend = {
|
|
11618
|
+
name: target.name,
|
|
11619
|
+
app: target.app
|
|
11620
|
+
};
|
|
11621
|
+
const { graph: resourceGraph, issues: resourceIssues } = await deriveResourceGraph({ configDir: path.join(projectRoot, resolveBackendPaths(backend.app, projectRoot).config) });
|
|
11622
|
+
if (resourceIssues.length > 0) throw new Error(`${resourceIssues.length} problem(s) in the declared resources:\n` + resourceIssues.map((i) => ` ${i.path} ${i.message}`).join("\n"));
|
|
11623
|
+
bundleDir = (await buildBundle({
|
|
11624
|
+
projectRoot,
|
|
11625
|
+
appName: backend.name,
|
|
11626
|
+
app: backend.app,
|
|
11627
|
+
runtimeRange: loaded.manifest.rebase,
|
|
11628
|
+
resources: resourceGraph,
|
|
11629
|
+
skipTypeCheck: opts.skipTypeCheck,
|
|
11630
|
+
log: (m) => progress(chalk.gray(m))
|
|
11631
|
+
})).outDir;
|
|
11170
11632
|
try {
|
|
11171
|
-
const
|
|
11172
|
-
|
|
11173
|
-
|
|
11633
|
+
const folded = await foldFrontendIntoBundle({
|
|
11634
|
+
projectRoot,
|
|
11635
|
+
manifest: loaded.manifest,
|
|
11636
|
+
bundleDir,
|
|
11637
|
+
log: (m) => progress(m)
|
|
11174
11638
|
});
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
console.log(res.logs ?? chalk.gray(" (no logs)"));
|
|
11179
|
-
console.log("");
|
|
11180
|
-
} catch (e) {
|
|
11181
|
-
reportError(e, "Failed to fetch runtime logs");
|
|
11639
|
+
for (const outcome of folded) progress(chalk.gray(` folded ${outcome.appName} in (${outcome.fileCount} file(s), served at ${outcome.path})`));
|
|
11640
|
+
} catch (err) {
|
|
11641
|
+
fail(err instanceof Error ? err.message : String(err), "Fix the frontend build, or pass --no-static to deploy the API alone.");
|
|
11182
11642
|
}
|
|
11183
|
-
return;
|
|
11184
11643
|
}
|
|
11644
|
+
await uploadAndTrigger({
|
|
11645
|
+
...opts,
|
|
11646
|
+
bundleDir
|
|
11647
|
+
});
|
|
11648
|
+
}
|
|
11649
|
+
/**
|
|
11650
|
+
* Pack a built bundle, upload it, and trigger the deploy.
|
|
11651
|
+
*
|
|
11652
|
+
* Shared by every managed deploy, whichever kind of app produced the bundle: a
|
|
11653
|
+
* backend and a static app differ in what is built and in the `kind` their
|
|
11654
|
+
* manifest carries, and in nothing after that. Keeping one tail is what makes
|
|
11655
|
+
* that true rather than nearly true — the folding step was once missing from
|
|
11656
|
+
* one of two callers producing the same artifact, and the deploy shipped a
|
|
11657
|
+
* bundle with no site in it.
|
|
11658
|
+
*/
|
|
11659
|
+
async function uploadAndTrigger(opts) {
|
|
11660
|
+
const { client, url, projectId, projectRef, bundleDir } = opts;
|
|
11661
|
+
const manifest = readBundleManifest(bundleDir);
|
|
11662
|
+
if (manifest.hooks?.native) {
|
|
11663
|
+
const names = (manifest.hooks.nativeModules ?? []).map((m) => m.name).join(", ");
|
|
11664
|
+
fail(`This bundle depends on native modules${names ? ` (${names})` : ""}, which the managed runtime cannot run.`, "Remove the native dependency, or deploy on the custom runtime.");
|
|
11665
|
+
}
|
|
11666
|
+
const tarPath = path.join(os.tmpdir(), `rebase-bundle-${Date.now()}.tar.gz`);
|
|
11667
|
+
const token = client.auth.getSession()?.accessToken;
|
|
11668
|
+
if (!token) fail("Not authenticated.", "Run `rebase cloud login`.");
|
|
11669
|
+
let bundleId;
|
|
11185
11670
|
try {
|
|
11186
|
-
|
|
11187
|
-
|
|
11188
|
-
|
|
11189
|
-
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
11671
|
+
await packBundle(bundleDir, tarPath);
|
|
11672
|
+
const sizeMb = (fs.statSync(tarPath).size / 1024 / 1024).toFixed(1);
|
|
11673
|
+
progress(chalk.gray(` Uploading bundle (${sizeMb} MB)...`));
|
|
11674
|
+
bundleId = await uploadBundle(url, token, projectId, tarPath);
|
|
11675
|
+
} catch (e) {
|
|
11676
|
+
fail(e instanceof Error ? e.message : String(e));
|
|
11677
|
+
return;
|
|
11678
|
+
} finally {
|
|
11679
|
+
fs.rmSync(tarPath, { force: true });
|
|
11680
|
+
}
|
|
11681
|
+
if (!isJsonMode()) {
|
|
11195
11682
|
console.log("");
|
|
11196
|
-
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11683
|
+
console.log(` 🚀 Triggering managed deployment for ${chalk.bold(projectRef)} (schema ${manifest.schemaVersion})...`);
|
|
11684
|
+
}
|
|
11685
|
+
let declaredApps = [];
|
|
11686
|
+
try {
|
|
11687
|
+
declaredApps = declaredAppsFrom(loadManifest(process.cwd()).manifest);
|
|
11688
|
+
} catch {}
|
|
11689
|
+
const body = bundleDeployBody({
|
|
11690
|
+
projectId,
|
|
11691
|
+
bundleId,
|
|
11692
|
+
manifest,
|
|
11693
|
+
app: opts.appName,
|
|
11694
|
+
message: opts.message,
|
|
11695
|
+
declaredApps
|
|
11696
|
+
});
|
|
11697
|
+
let deploymentId;
|
|
11698
|
+
let managed;
|
|
11699
|
+
try {
|
|
11700
|
+
const res = await client.functions.invoke("deploy", body);
|
|
11701
|
+
if (!res?.deployment?.id) fail("Control plane did not return a deployment id.");
|
|
11702
|
+
deploymentId = String(res.deployment.id);
|
|
11703
|
+
managed = res.managed === true;
|
|
11201
11704
|
} catch (e) {
|
|
11202
|
-
reportError(e, "
|
|
11705
|
+
reportError(e, "Managed deploy failed to start");
|
|
11706
|
+
}
|
|
11707
|
+
if (!opts.follow) {
|
|
11708
|
+
emit(() => {
|
|
11709
|
+
console.log(chalk.green(` ✓ Managed deploy started (deployment ${deploymentId}).`));
|
|
11710
|
+
console.log(chalk.gray(" Not following (--no-follow). Track it with `rebase cloud logs`."));
|
|
11711
|
+
}, {
|
|
11712
|
+
success: true,
|
|
11713
|
+
deploymentId,
|
|
11714
|
+
managed,
|
|
11715
|
+
following: false
|
|
11716
|
+
});
|
|
11717
|
+
return;
|
|
11203
11718
|
}
|
|
11719
|
+
if (!isJsonMode()) {
|
|
11720
|
+
console.log(chalk.green(` ✓ Managed deploy started (deployment ${deploymentId}).`));
|
|
11721
|
+
console.log(chalk.gray(" Streaming build logs (Ctrl-C to stop watching — the build keeps running):"));
|
|
11722
|
+
console.log("");
|
|
11723
|
+
}
|
|
11724
|
+
const status = await streamBuildLogs(client, deploymentId, {
|
|
11725
|
+
quiet: isJsonMode(),
|
|
11726
|
+
timeoutMs: opts.timeoutMs
|
|
11727
|
+
});
|
|
11728
|
+
emit(() => {}, {
|
|
11729
|
+
success: true,
|
|
11730
|
+
deploymentId,
|
|
11731
|
+
managed,
|
|
11732
|
+
following: true,
|
|
11733
|
+
status
|
|
11734
|
+
});
|
|
11735
|
+
}
|
|
11736
|
+
function pick(row, ...keys) {
|
|
11737
|
+
for (const key of keys) {
|
|
11738
|
+
const raw = row?.[key];
|
|
11739
|
+
if (typeof raw === "string" && raw.trim() !== "") return raw.trim();
|
|
11740
|
+
}
|
|
11741
|
+
}
|
|
11742
|
+
/** Rough age of a timestamp, for "…uploaded 6d ago". Undefined if unreadable. */
|
|
11743
|
+
function timeAgo(value, now) {
|
|
11744
|
+
if (value === void 0) return void 0;
|
|
11745
|
+
const then = value instanceof Date ? value.getTime() : new Date(value).getTime();
|
|
11746
|
+
if (Number.isNaN(then)) return void 0;
|
|
11747
|
+
const ms = now.getTime() - then;
|
|
11748
|
+
if (ms < 0) return void 0;
|
|
11749
|
+
const minutes = Math.floor(ms / 6e4);
|
|
11750
|
+
if (minutes < 1) return "just now";
|
|
11751
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
11752
|
+
const hours = Math.floor(minutes / 60);
|
|
11753
|
+
if (hours < 24) return `${hours}h ago`;
|
|
11754
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
11204
11755
|
}
|
|
11205
|
-
//#endregion
|
|
11206
|
-
//#region src/commands/cloud/orgs.ts
|
|
11207
11756
|
/**
|
|
11208
|
-
*
|
|
11757
|
+
* Whether this project runs on the managed runtime.
|
|
11758
|
+
*
|
|
11759
|
+
* `runtimeMode` on the project row is the authority — the control plane writes
|
|
11760
|
+
* it. The bundle-id fallback covers a control plane that does not return the
|
|
11761
|
+
* field: a successful deploy that served a bundle only happens on the managed
|
|
11762
|
+
* path.
|
|
11209
11763
|
*/
|
|
11210
|
-
|
|
11211
|
-
|
|
11212
|
-
|
|
11213
|
-
|
|
11214
|
-
|
|
11215
|
-
|
|
11216
|
-
|
|
11217
|
-
|
|
11218
|
-
|
|
11219
|
-
|
|
11220
|
-
|
|
11221
|
-
|
|
11222
|
-
|
|
11223
|
-
|
|
11224
|
-
|
|
11225
|
-
|
|
11764
|
+
function isManagedProject(project, latest) {
|
|
11765
|
+
if (pick(project, "runtimeMode", "runtime_mode") === "managed") return true;
|
|
11766
|
+
return latest?.status === "success" && pick(latest, "bundleId", "bundle_id") !== void 0;
|
|
11767
|
+
}
|
|
11768
|
+
/** What a `deploy` with nothing attached will build, in the words to print. */
|
|
11769
|
+
function planBareDeploy(project, latest, now) {
|
|
11770
|
+
const projectRow = project;
|
|
11771
|
+
const deploymentRow = latest;
|
|
11772
|
+
const managed = isManagedProject(project, latest);
|
|
11773
|
+
const repo = pick(projectRow, "gitRepoUrl", "git_repo_url");
|
|
11774
|
+
if (repo) {
|
|
11775
|
+
const branch = pick(projectRow, "gitBranch", "git_branch");
|
|
11776
|
+
return {
|
|
11777
|
+
managed,
|
|
11778
|
+
source: "git",
|
|
11779
|
+
lines: [`Building from git: ${repo}${branch ? ` (${branch})` : ""}.`]
|
|
11780
|
+
};
|
|
11781
|
+
}
|
|
11782
|
+
if (pick(deploymentRow, "sourceRef", "source_ref")) {
|
|
11783
|
+
const age = timeAgo(latest?.createdAt ?? latest?.created_at, now);
|
|
11784
|
+
return {
|
|
11785
|
+
managed,
|
|
11786
|
+
source: "snapshot",
|
|
11787
|
+
lines: [`Rebuilding the stored source archive${latest?.id !== void 0 ? ` from deployment ${latest.id}` : ""}${age ? `, uploaded ${age}` : ""}.`, "This directory is NOT uploaded — pass `--source .` to build what is on disk."]
|
|
11788
|
+
};
|
|
11226
11789
|
}
|
|
11790
|
+
return {
|
|
11791
|
+
managed,
|
|
11792
|
+
source: "none",
|
|
11793
|
+
lines: ["This project has no git repository configured and no stored source archive to rebuild.", "Upload this directory with `--source .`, or set a repository URL in the project settings."]
|
|
11794
|
+
};
|
|
11227
11795
|
}
|
|
11228
|
-
|
|
11229
|
-
|
|
11796
|
+
/** `code` of the warning below, and the field name it sets in the payload. */
|
|
11797
|
+
var EJECTS_MANAGED_RUNTIME = "ejects_managed_runtime";
|
|
11798
|
+
/** The one sentence that says a source build undoes `runtimeMode: managed`. */
|
|
11799
|
+
function ejectWarning(projectRef) {
|
|
11800
|
+
return {
|
|
11801
|
+
code: EJECTS_MANAGED_RUNTIME,
|
|
11802
|
+
message: `${projectRef} runs on the managed runtime — this build ejects it to a custom container.`,
|
|
11803
|
+
hint: "Use `rebase cloud deploy --bundle` to stay on managed."
|
|
11804
|
+
};
|
|
11805
|
+
}
|
|
11806
|
+
/**
|
|
11807
|
+
* Why a container-image deploy of a managed project is refused — or `undefined`
|
|
11808
|
+
* to let it through.
|
|
11809
|
+
*
|
|
11810
|
+
* Every path below this point builds a container image, and a successful one
|
|
11811
|
+
* sets `runtimeMode: "custom"` server-side. So the question is never "which flag
|
|
11812
|
+
* was used" but "did the caller ask to leave the managed runtime", and only
|
|
11813
|
+
* `--force` answers it.
|
|
11814
|
+
*
|
|
11815
|
+
* `--source` used to be read as answering it too, on the theory that uploading a
|
|
11816
|
+
* build context is self-evidently a deliberate eject. It is not: `--source`
|
|
11817
|
+
* picks *which source* gets built — this directory, rather than the stale
|
|
11818
|
+
* archive the control plane is holding — and the eject is a side effect of the
|
|
11819
|
+
* answer. That is exactly how a live project got flipped to `custom` by someone
|
|
11820
|
+
* whose actual intent was "deploy what I have here", and it is the same
|
|
11821
|
+
* ignorance the bare form is refused for. Same ignorance, same refusal.
|
|
11822
|
+
*/
|
|
11823
|
+
function ejectRefusal(opts, projectRef) {
|
|
11824
|
+
if (!opts.managed || opts.force) return void 0;
|
|
11825
|
+
const eject = "To eject on purpose, add `--force`.";
|
|
11826
|
+
if (opts.source) return {
|
|
11827
|
+
message: `${projectRef} runs on the managed runtime, and \`--source\` builds a container image from this directory — which ejects it from managed. Picking a build method is not the same as asking to leave the runtime.`,
|
|
11828
|
+
hint: `Deploy this directory to the managed runtime with \`rebase cloud deploy --bundle\`. ${eject}`,
|
|
11829
|
+
code: "managed_project"
|
|
11830
|
+
};
|
|
11831
|
+
return {
|
|
11832
|
+
message: `${projectRef} runs on the managed runtime, and a plain \`rebase cloud deploy\` builds a container image instead — ejecting it from managed, from source the control plane already holds rather than this directory.`,
|
|
11833
|
+
hint: `Redeploy it with \`rebase cloud deploy --bundle\`. ${eject} \`--source . --force\` builds this directory; \`--force\` alone builds what the control plane holds.`,
|
|
11834
|
+
code: "managed_project"
|
|
11835
|
+
};
|
|
11836
|
+
}
|
|
11837
|
+
/**
|
|
11838
|
+
* Which warnings a container-image deploy has earned.
|
|
11839
|
+
*
|
|
11840
|
+
* Pure, and separate from the printing, because the printing is what went
|
|
11841
|
+
* wrong: the eject warning used to be written inline behind `!isJsonMode()`, so
|
|
11842
|
+
* the fact that a deploy ejects a managed project existed only as a side effect
|
|
11843
|
+
* of a TTY being attached. Deciding here, emitting once at the call site, means
|
|
11844
|
+
* the decision cannot be output-mode-dependent again.
|
|
11845
|
+
*
|
|
11846
|
+
* The condition is just `managed`: anything reaching this point is a container
|
|
11847
|
+
* image build that `ejectRefusal` has already let through, and on a managed
|
|
11848
|
+
* project that is an eject however it was spelled. A caller who passed `--force`
|
|
11849
|
+
* knows — the warning is for the transcript and the payload, which is what
|
|
11850
|
+
* anyone reviewing the deploy afterwards actually reads.
|
|
11851
|
+
*/
|
|
11852
|
+
function deployWarnings(opts, projectRef) {
|
|
11853
|
+
return opts.managed ? [ejectWarning(projectRef)] : [];
|
|
11854
|
+
}
|
|
11855
|
+
/** The warning half of a deploy's JSON payload — merged into whatever it emits. */
|
|
11856
|
+
function warningPayload(warnings) {
|
|
11857
|
+
return {
|
|
11858
|
+
warnings: warnings.map((w) => ({
|
|
11859
|
+
code: w.code,
|
|
11860
|
+
message: w.message,
|
|
11861
|
+
hint: w.hint ?? null
|
|
11862
|
+
})),
|
|
11863
|
+
ejectsManagedRuntime: warnings.some((w) => w.code === EJECTS_MANAGED_RUNTIME)
|
|
11864
|
+
};
|
|
11865
|
+
}
|
|
11866
|
+
/**
|
|
11867
|
+
* Read the two rows the preflight needs.
|
|
11868
|
+
*
|
|
11869
|
+
* Best effort by construction: a preflight that cannot read is a preflight that
|
|
11870
|
+
* says nothing, never a deploy that fails. The managed refusal rides on the same
|
|
11871
|
+
* read, so an unreadable project falls through to the old behaviour rather than
|
|
11872
|
+
* blocking a deploy on a lookup.
|
|
11873
|
+
*/
|
|
11874
|
+
async function readDeployContext(client, projectId) {
|
|
11230
11875
|
try {
|
|
11231
|
-
const
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
console.log(chalk.gray(" You are not a member of any organization."));
|
|
11239
|
-
console.log("");
|
|
11240
|
-
return;
|
|
11241
|
-
}
|
|
11242
|
-
for (const o of orgs) {
|
|
11243
|
-
const marker = String(o.id) === active ? chalk.green(" ●") : " ";
|
|
11244
|
-
console.log(`${marker}${chalk.bold(o.name ?? "(unnamed)")} ${chalk.gray(`[${o.id}]`)}${o.slug ? chalk.gray(` ${o.slug}`) : ""}`);
|
|
11245
|
-
}
|
|
11246
|
-
console.log("");
|
|
11247
|
-
note(chalk.gray("● = active organization. Switch with `rebase cloud use <id>`."));
|
|
11248
|
-
console.log("");
|
|
11249
|
-
}, {
|
|
11250
|
-
activeOrg: active ?? null,
|
|
11251
|
-
organizations: orgs.map((o) => ({
|
|
11252
|
-
id: String(o.id),
|
|
11253
|
-
name: o.name ?? null,
|
|
11254
|
-
slug: o.slug ?? null,
|
|
11255
|
-
active: String(o.id) === active
|
|
11256
|
-
}))
|
|
11257
|
-
});
|
|
11258
|
-
} catch (e) {
|
|
11259
|
-
reportError(e, "Failed to list organizations");
|
|
11876
|
+
const [project, latest] = await Promise.all([client.data.collection("projects").findById(projectId), latestDeployment(client, projectId)]);
|
|
11877
|
+
return {
|
|
11878
|
+
project,
|
|
11879
|
+
latest
|
|
11880
|
+
};
|
|
11881
|
+
} catch {
|
|
11882
|
+
return {};
|
|
11260
11883
|
}
|
|
11261
11884
|
}
|
|
11262
|
-
|
|
11263
|
-
|
|
11264
|
-
|
|
11265
|
-
|
|
11266
|
-
|
|
11267
|
-
|
|
11268
|
-
|
|
11269
|
-
|
|
11885
|
+
/**
|
|
11886
|
+
* Whether this repository's backend declares the managed runtime.
|
|
11887
|
+
*
|
|
11888
|
+
* Deliberately quiet: a directory that is not a Rebase project, or whose
|
|
11889
|
+
* manifest does not parse, simply does not route this way — `rebase build` is
|
|
11890
|
+
* where a broken manifest gets reported, and a deploy refusing on one before it
|
|
11891
|
+
* has even said what it is doing would be the wrong place to find out.
|
|
11892
|
+
*/
|
|
11893
|
+
function declaresManagedRuntime(appName) {
|
|
11894
|
+
try {
|
|
11895
|
+
const projectRoot = findProjectRoot();
|
|
11896
|
+
if (!projectRoot) return false;
|
|
11897
|
+
const manifest = loadManifest(projectRoot).manifest;
|
|
11898
|
+
if (appName) {
|
|
11899
|
+
const app = manifest.apps[appName];
|
|
11900
|
+
if (app?.type === "static") return true;
|
|
11901
|
+
if (app?.type === "backend") return app.runtime === "managed";
|
|
11902
|
+
}
|
|
11903
|
+
const backend = findBackendApp(manifest);
|
|
11904
|
+
if (!backend) return Object.keys(manifest.apps).length > 0;
|
|
11905
|
+
return backend.app.runtime === "managed";
|
|
11906
|
+
} catch {
|
|
11907
|
+
return false;
|
|
11908
|
+
}
|
|
11909
|
+
}
|
|
11910
|
+
/**
|
|
11911
|
+
* Every flag `rebase cloud deploy` accepts.
|
|
11912
|
+
*
|
|
11913
|
+
* Hoisted out of the `parseCloudArgs` call so that one declaration serves three
|
|
11914
|
+
* readers: the parser, `action-help.ts`'s page for this command, and the test
|
|
11915
|
+
* that holds the two to each other. A flag added here with no line in the help
|
|
11916
|
+
* page is a failing test rather than a flag nobody can discover.
|
|
11917
|
+
*/
|
|
11918
|
+
var DEPLOY_FLAGS = {
|
|
11919
|
+
"--no-follow": Boolean,
|
|
11920
|
+
"--wait": Boolean,
|
|
11921
|
+
"--timeout": String,
|
|
11922
|
+
"--source": String,
|
|
11923
|
+
"--message": String,
|
|
11924
|
+
"--bundle": Boolean,
|
|
11925
|
+
"--bundle-dir": String,
|
|
11926
|
+
"--skip-type-check": Boolean,
|
|
11927
|
+
"--force": Boolean,
|
|
11928
|
+
"-m": "--message"
|
|
11929
|
+
};
|
|
11930
|
+
/**
|
|
11931
|
+
* `rebase cloud deploy [app]` — its flags, and which app of this repository the
|
|
11932
|
+
* line named.
|
|
11933
|
+
*
|
|
11934
|
+
* Parsed through `parseCloudArgs` rather than `arg` directly, and the reason is
|
|
11935
|
+
* the positional. This command used to parse `rawArgs.slice(2)` permissively
|
|
11936
|
+
* and read `_[0]` as the app name — but `rawArgs` is the WHOLE `process.argv`,
|
|
11937
|
+
* so `_` opens with the command words themselves. `_[0]` was therefore the
|
|
11938
|
+
* literal string `"cloud"` on every run, which then went to `selectDeployApp`
|
|
11939
|
+
* and came back as:
|
|
11940
|
+
*
|
|
11941
|
+
* This repository declares no app named "cloud". It declares: backend, web.
|
|
11942
|
+
*
|
|
11943
|
+
* So the documented `rebase cloud deploy --bundle` failed on every project that
|
|
11944
|
+
* did not happen to declare an app called `cloud`, `rebase cloud deploy web`
|
|
11945
|
+
* could not reach `web`, and the refusal named the user's real apps — reading
|
|
11946
|
+
* as a fault in their `rebase.json` rather than in the CLI's own parse.
|
|
11947
|
+
*
|
|
11948
|
+
* `commandWords` counts from `cloud` itself, so `cloud deploy` is 2, and it is
|
|
11949
|
+
* applied to the PARSED positionals: a flag written before the group no longer
|
|
11950
|
+
* shifts the app name either.
|
|
11951
|
+
*/
|
|
11952
|
+
function resolveDeployArgs(rawArgs) {
|
|
11953
|
+
const { flags, positionals } = parseCloudArgs({
|
|
11954
|
+
spec: DEPLOY_FLAGS,
|
|
11955
|
+
rawArgs,
|
|
11956
|
+
commandWords: 2,
|
|
11957
|
+
command: "cloud deploy",
|
|
11958
|
+
maxPositionals: 1
|
|
11270
11959
|
});
|
|
11960
|
+
return {
|
|
11961
|
+
flags,
|
|
11962
|
+
appName: positionals[0]
|
|
11963
|
+
};
|
|
11964
|
+
}
|
|
11965
|
+
async function deployCommand(rawArgs, projectRef) {
|
|
11966
|
+
const { flags: args, appName } = resolveDeployArgs(rawArgs);
|
|
11967
|
+
if (args["--wait"] && args["--no-follow"]) fail("--wait and --no-follow ask for opposite things.", "`deploy` follows by default — pass neither, or `--no-follow` to return as soon as the build is triggered.", "usage");
|
|
11271
11968
|
const { client, url } = await requireClient(rawArgs);
|
|
11272
|
-
const
|
|
11273
|
-
|
|
11274
|
-
|
|
11275
|
-
|
|
11276
|
-
|
|
11277
|
-
|
|
11278
|
-
|
|
11969
|
+
const projectId = await resolveProjectRef(projectRef, client);
|
|
11970
|
+
const declaredManaged = !args["--source"] && !args["--bundle"] && declaresManagedRuntime(appName);
|
|
11971
|
+
if (args["--bundle"] || declaredManaged) {
|
|
11972
|
+
if (args["--bundle"] && args["--source"]) fail("--bundle and --source cannot be combined: one is a managed bundle, the other a source build.");
|
|
11973
|
+
if (declaredManaged && !isJsonMode()) console.log(chalk.gray(" rebase.json declares runtime: managed — deploying a bundle."));
|
|
11974
|
+
await deployBundle({
|
|
11975
|
+
client,
|
|
11976
|
+
url,
|
|
11977
|
+
projectId,
|
|
11978
|
+
projectRef,
|
|
11979
|
+
bundleDir: args["--bundle-dir"],
|
|
11980
|
+
message: args["--message"],
|
|
11981
|
+
appName,
|
|
11982
|
+
skipTypeCheck: args["--skip-type-check"] === true,
|
|
11983
|
+
follow: args["--no-follow"] !== true,
|
|
11984
|
+
timeoutMs: resolveDeployTimeout(args["--timeout"])
|
|
11279
11985
|
});
|
|
11986
|
+
return;
|
|
11280
11987
|
}
|
|
11281
|
-
const
|
|
11282
|
-
const
|
|
11283
|
-
|
|
11284
|
-
|
|
11285
|
-
|
|
11286
|
-
|
|
11287
|
-
|
|
11288
|
-
|
|
11289
|
-
|
|
11290
|
-
|
|
11291
|
-
|
|
11292
|
-
|
|
11293
|
-
|
|
11294
|
-
|
|
11295
|
-
|
|
11296
|
-
|
|
11297
|
-
|
|
11298
|
-
|
|
11299
|
-
|
|
11300
|
-
|
|
11301
|
-
|
|
11988
|
+
const { project, latest } = await readDeployContext(client, projectId);
|
|
11989
|
+
const plan = planBareDeploy(project, latest, /* @__PURE__ */ new Date());
|
|
11990
|
+
const eject = {
|
|
11991
|
+
managed: plan.managed,
|
|
11992
|
+
source: Boolean(args["--source"]),
|
|
11993
|
+
force: args["--force"] === true
|
|
11994
|
+
};
|
|
11995
|
+
const refusal = ejectRefusal(eject, projectRef);
|
|
11996
|
+
if (refusal) fail(refusal.message, refusal.hint, refusal.code);
|
|
11997
|
+
const warnings = deployWarnings(eject, projectRef);
|
|
11998
|
+
for (const w of warnings) warn(w.message, w.hint);
|
|
11999
|
+
if (!args["--source"] && !isJsonMode()) {
|
|
12000
|
+
console.log("");
|
|
12001
|
+
for (const line of plan.lines) console.log(chalk.gray(` ${line}`));
|
|
12002
|
+
}
|
|
12003
|
+
let source;
|
|
12004
|
+
if (args["--source"]) {
|
|
12005
|
+
const tarPath = await createSourceTarball(args["--source"]);
|
|
12006
|
+
try {
|
|
12007
|
+
const token = client.auth.getSession()?.accessToken;
|
|
12008
|
+
if (!token) fail("Not authenticated.", "Run `rebase cloud login`.");
|
|
12009
|
+
source = await uploadSource(url, token, projectId, tarPath);
|
|
12010
|
+
} finally {
|
|
12011
|
+
fs.rmSync(tarPath, { force: true });
|
|
12012
|
+
}
|
|
12013
|
+
}
|
|
12014
|
+
if (!isJsonMode()) {
|
|
12015
|
+
console.log("");
|
|
12016
|
+
console.log(` 🚀 Triggering deployment for project ${chalk.bold(projectRef)}${source ? " from uploaded source" : ""}...`);
|
|
11302
12017
|
}
|
|
11303
|
-
}
|
|
11304
|
-
|
|
11305
|
-
|
|
11306
|
-
|
|
11307
|
-
|
|
12018
|
+
const body = { projectId };
|
|
12019
|
+
if (source) body.source = source;
|
|
12020
|
+
if (args["--message"]) body.message = args["--message"];
|
|
12021
|
+
body.client = "cli";
|
|
12022
|
+
const frameworkVersion = resolveFrameworkVersion(args["--source"] ?? process.cwd());
|
|
12023
|
+
if (frameworkVersion) body.frameworkVersion = frameworkVersion;
|
|
12024
|
+
let triggered;
|
|
11308
12025
|
try {
|
|
11309
|
-
const
|
|
11310
|
-
|
|
11311
|
-
|
|
11312
|
-
|
|
12026
|
+
const res = await client.functions.invoke("deploy", body);
|
|
12027
|
+
if (!res?.deployment?.id) fail("Control plane did not return a deployment id.");
|
|
12028
|
+
triggered = {
|
|
12029
|
+
deploymentId: String(res.deployment.id),
|
|
12030
|
+
deduplicated: res.deduplicated === true
|
|
12031
|
+
};
|
|
12032
|
+
} catch (e) {
|
|
12033
|
+
triggered = resolveTriggerFailure(e);
|
|
12034
|
+
}
|
|
12035
|
+
const { deploymentId, deduplicated } = triggered;
|
|
12036
|
+
if (!isJsonMode()) console.log(chalk.gray(deduplicated ? ` Deployment ${deploymentId} is already running — following it.` : ` Deployment ${deploymentId} created.${frameworkVersion ? ` (@rebasepro/* ${frameworkVersion})` : ""}`));
|
|
12037
|
+
if (args["--no-follow"]) {
|
|
11313
12038
|
emit(() => {
|
|
11314
|
-
console.log("");
|
|
11315
|
-
console.log(chalk.bold(` 👥 Members — org ${org}`));
|
|
11316
|
-
console.log("");
|
|
11317
|
-
if (members.length === 0) {
|
|
11318
|
-
console.log(chalk.gray(" No members found."));
|
|
11319
|
-
console.log("");
|
|
11320
|
-
return;
|
|
11321
|
-
}
|
|
11322
|
-
for (const m of members) console.log(` ${chalk.bold(m.userId ?? "?")} ${colorStatus(m.role)}`);
|
|
12039
|
+
console.log(chalk.gray(" Not following logs (--no-follow). Check status with `rebase cloud logs`."));
|
|
11323
12040
|
console.log("");
|
|
11324
12041
|
}, {
|
|
11325
|
-
|
|
11326
|
-
|
|
11327
|
-
|
|
11328
|
-
|
|
11329
|
-
|
|
11330
|
-
}))
|
|
12042
|
+
deploymentId,
|
|
12043
|
+
deduplicated,
|
|
12044
|
+
frameworkVersion: frameworkVersion ?? null,
|
|
12045
|
+
following: false,
|
|
12046
|
+
...warningPayload(warnings)
|
|
11331
12047
|
});
|
|
11332
|
-
|
|
11333
|
-
reportError(e, "Failed to list members");
|
|
12048
|
+
return;
|
|
11334
12049
|
}
|
|
12050
|
+
if (!isJsonMode()) {
|
|
12051
|
+
console.log(chalk.gray(" Streaming build logs (Ctrl-C to stop watching — the build keeps running):"));
|
|
12052
|
+
console.log("");
|
|
12053
|
+
}
|
|
12054
|
+
const status = await streamBuildLogs(client, deploymentId, {
|
|
12055
|
+
quiet: isJsonMode(),
|
|
12056
|
+
timeoutMs: resolveDeployTimeout(args["--timeout"])
|
|
12057
|
+
});
|
|
12058
|
+
emit(() => {}, {
|
|
12059
|
+
deploymentId,
|
|
12060
|
+
deduplicated,
|
|
12061
|
+
frameworkVersion: frameworkVersion ?? null,
|
|
12062
|
+
following: true,
|
|
12063
|
+
status,
|
|
12064
|
+
...warningPayload(warnings)
|
|
12065
|
+
});
|
|
11335
12066
|
}
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
|
|
11340
|
-
|
|
11341
|
-
"list",
|
|
11342
|
-
"create",
|
|
11343
|
-
"members"
|
|
11344
|
-
], () => {
|
|
11345
|
-
console.log(`
|
|
11346
|
-
${chalk.bold("rebase cloud orgs")} — Manage organizations
|
|
11347
|
-
|
|
11348
|
-
${chalk.green.bold("Commands")}
|
|
11349
|
-
${chalk.blue.bold("list")} List organizations you belong to
|
|
11350
|
-
${chalk.blue.bold("create")} Create a new organization ${chalk.gray("(--name, --slug)")}
|
|
11351
|
-
${chalk.blue.bold("members")} List members of the active organization
|
|
11352
|
-
`);
|
|
12067
|
+
/** `--timeout <seconds>` for a deploy, or the 15-minute default. */
|
|
12068
|
+
function resolveDeployTimeout(value) {
|
|
12069
|
+
return resolveTimeoutMs(value, {
|
|
12070
|
+
fallbackMs: POLL_TIMEOUT_MS,
|
|
12071
|
+
command: "cloud deploy"
|
|
11353
12072
|
});
|
|
11354
12073
|
}
|
|
11355
|
-
//#endregion
|
|
11356
|
-
//#region src/commands/cloud/databases.ts
|
|
11357
12074
|
/**
|
|
11358
|
-
*
|
|
12075
|
+
* Turn a failed trigger into either a deployment to follow, or an exit.
|
|
11359
12076
|
*
|
|
11360
|
-
*
|
|
11361
|
-
*
|
|
11362
|
-
*
|
|
11363
|
-
*
|
|
12077
|
+
* The 409 is the interesting one. A deploy trigger can reach the control plane
|
|
12078
|
+
* twice without anybody asking twice — the SDK transport replays a request once
|
|
12079
|
+
* after refreshing an expired token, and any lost response has the same effect
|
|
12080
|
+
* — so "a deployment is already in progress" was routinely describing the
|
|
12081
|
+
* deployment this very command had just created. With no id in the message the
|
|
12082
|
+
* only available reading was "someone else is deploying, back off", and the
|
|
12083
|
+
* build stream was lost either way.
|
|
12084
|
+
*
|
|
12085
|
+
* So: if the control plane says the blocking deployment is ours, we attach to
|
|
12086
|
+
* it. If it is not ours, we still name it, because "which one, since when, from
|
|
12087
|
+
* where" is the difference between an actionable refusal and a dead end.
|
|
11364
12088
|
*/
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
11370
|
-
|
|
11371
|
-
|
|
11372
|
-
|
|
11373
|
-
|
|
11374
|
-
case "info":
|
|
11375
|
-
await dbInfo(rawArgs);
|
|
11376
|
-
break;
|
|
11377
|
-
case "test":
|
|
11378
|
-
await testDatabase(rawArgs);
|
|
11379
|
-
break;
|
|
11380
|
-
case "backup":
|
|
11381
|
-
await backupCommand(rawArgs);
|
|
11382
|
-
break;
|
|
11383
|
-
case "pitr":
|
|
11384
|
-
await pitrCommand(rawArgs);
|
|
11385
|
-
break;
|
|
11386
|
-
case "--help":
|
|
11387
|
-
printDbHelp();
|
|
11388
|
-
break;
|
|
11389
|
-
default: fail(`Unknown db command: ${subcommand}`, "Run `rebase cloud db --help`.", "unknown_command");
|
|
12089
|
+
function resolveTriggerFailure(e) {
|
|
12090
|
+
const err = e;
|
|
12091
|
+
if (err?.status === 409) {
|
|
12092
|
+
const blocking = err.details?.deployment;
|
|
12093
|
+
if (blocking?.id && blocking.mine) return {
|
|
12094
|
+
deploymentId: String(blocking.id),
|
|
12095
|
+
deduplicated: true
|
|
12096
|
+
};
|
|
12097
|
+
fail(blocking?.id ? `Deployment ${blocking.id} is already in progress for this project${blocking.triggerSource && blocking.triggerSource !== "unknown" ? `, triggered from the ${blocking.triggerSource}` : ""}${blocking.createdAt ? ` at ${fmtDate(blocking.createdAt)}` : ""}.` : "A deployment is already in progress for this project.", blocking?.id ? `Follow it with \`rebase cloud logs -f\`, or stop it with \`rebase cloud cancel ${blocking.id}\`.` : "Follow it with `rebase cloud logs -f`.", "deploy_in_progress");
|
|
11390
12098
|
}
|
|
12099
|
+
if (err?.status === 402) fail(err.message || "Payment required before deploying.", "Attach a card once with `rebase cloud billing setup`, then deploy again.", "payment_required");
|
|
12100
|
+
if (err?.status === 400 && err.details?.intakeCode) fail(err.message || "This bundle was refused.", err.details.hint ?? "Run `rebase cloud resources` to see what this project is given.", err.details.intakeCode);
|
|
12101
|
+
reportError(e, "Failed to trigger deployment");
|
|
11391
12102
|
}
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
|
|
11395
|
-
|
|
11396
|
-
|
|
11397
|
-
|
|
11398
|
-
|
|
11399
|
-
|
|
11400
|
-
|
|
11401
|
-
|
|
11402
|
-
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
|
|
12103
|
+
/**
|
|
12104
|
+
* Poll a deployment record and print new log output as it arrives. Returns the
|
|
12105
|
+
* terminal status; a non-success still exits non-zero, as it always has.
|
|
12106
|
+
*
|
|
12107
|
+
* `quiet` follows without printing — JSON mode, where the log stream would
|
|
12108
|
+
* corrupt the one object the caller is parsing.
|
|
12109
|
+
*/
|
|
12110
|
+
async function streamBuildLogs(client, deploymentId, opts = {}) {
|
|
12111
|
+
const quiet = opts.quiet === true;
|
|
12112
|
+
const timeoutMs = opts.timeoutMs ?? POLL_TIMEOUT_MS;
|
|
12113
|
+
let printed = 0;
|
|
12114
|
+
const started = Date.now();
|
|
12115
|
+
for (;;) {
|
|
12116
|
+
let dep;
|
|
12117
|
+
try {
|
|
12118
|
+
dep = await client.data.collection("deployments").findById(deploymentId);
|
|
12119
|
+
} catch (e) {
|
|
12120
|
+
reportError(e, "Failed to read deployment status");
|
|
12121
|
+
}
|
|
12122
|
+
if (!dep) fail(`Deployment ${deploymentId} disappeared.`, void 0, "not_found");
|
|
12123
|
+
const logs = dep.logs ?? "";
|
|
12124
|
+
if (!quiet && logs.length > printed) process.stdout.write(logs.slice(printed));
|
|
12125
|
+
printed = logs.length;
|
|
12126
|
+
if (dep.status && dep.status !== "deploying") {
|
|
12127
|
+
if (dep.status !== "success") {
|
|
12128
|
+
if (quiet) {
|
|
12129
|
+
printJson({ error: {
|
|
12130
|
+
message: `Deployment ${deploymentId} ${dep.status}.`,
|
|
12131
|
+
code: "deploy_failed",
|
|
12132
|
+
status: null,
|
|
12133
|
+
deploymentId,
|
|
12134
|
+
logs
|
|
12135
|
+
} });
|
|
12136
|
+
process.exit(1);
|
|
12137
|
+
}
|
|
11407
12138
|
console.log("");
|
|
11408
|
-
|
|
11409
|
-
|
|
11410
|
-
|
|
11411
|
-
console.log(` ${chalk.bold(d.type ?? "unknown")} ${chalk.gray(`[${d.id}]`)} ${colorStatus(d.connectionStatus)}`);
|
|
11412
|
-
keyValues([["SSH tunnel", d.useSshTunnel ? "yes" : void 0], ["PITR", d.pitrEnabled ? "enabled" : void 0]]);
|
|
12139
|
+
console.log(chalk.bold.red(` ✗ Deployment ${dep.status}`));
|
|
12140
|
+
console.log("");
|
|
12141
|
+
process.exit(1);
|
|
11413
12142
|
}
|
|
11414
|
-
|
|
11415
|
-
|
|
11416
|
-
|
|
11417
|
-
|
|
11418
|
-
id: String(d.id),
|
|
11419
|
-
type: d.type ?? null,
|
|
11420
|
-
connectionStatus: d.connectionStatus ?? null,
|
|
11421
|
-
useSshTunnel: Boolean(d.useSshTunnel),
|
|
11422
|
-
pitrEnabled: Boolean(d.pitrEnabled)
|
|
11423
|
-
}))
|
|
11424
|
-
});
|
|
11425
|
-
} catch (e) {
|
|
11426
|
-
reportError(e, "Failed to list databases");
|
|
11427
|
-
}
|
|
11428
|
-
}
|
|
11429
|
-
async function createDatabase(rawArgs) {
|
|
11430
|
-
const args = arg({
|
|
11431
|
-
"--type": String,
|
|
11432
|
-
"--connection-string": String,
|
|
11433
|
-
"--project": String,
|
|
11434
|
-
"-p": "--project"
|
|
11435
|
-
}, {
|
|
11436
|
-
argv: rawArgs.slice(4),
|
|
11437
|
-
permissive: true
|
|
11438
|
-
});
|
|
11439
|
-
const { client } = await requireClient(rawArgs);
|
|
11440
|
-
const projectId = await requireProject(rawArgs, client);
|
|
11441
|
-
const projectRef = displayProjectRef(rawArgs);
|
|
11442
|
-
let type = args["--type"];
|
|
11443
|
-
if (!type) {
|
|
11444
|
-
requireInteractive("a database type", "--type <managed|byodb>");
|
|
11445
|
-
const { picked } = await inquirer.prompt([{
|
|
11446
|
-
type: "select",
|
|
11447
|
-
name: "picked",
|
|
11448
|
-
message: "Database type:",
|
|
11449
|
-
choices: [{
|
|
11450
|
-
name: "SaaS Managed (provisioned for you)",
|
|
11451
|
-
value: "managed"
|
|
11452
|
-
}, {
|
|
11453
|
-
name: "Bring Your Own DB (external PostgreSQL)",
|
|
11454
|
-
value: "byodb"
|
|
11455
|
-
}]
|
|
11456
|
-
}]);
|
|
11457
|
-
type = picked;
|
|
11458
|
-
}
|
|
11459
|
-
let connectionString = args["--connection-string"];
|
|
11460
|
-
if (type === "byodb" && !connectionString) {
|
|
11461
|
-
requireInteractive("a connection string", "--connection-string <url>");
|
|
11462
|
-
const { cs } = await inquirer.prompt([{
|
|
11463
|
-
type: "input",
|
|
11464
|
-
name: "cs",
|
|
11465
|
-
message: "PostgreSQL connection string:"
|
|
11466
|
-
}]);
|
|
11467
|
-
connectionString = cs?.trim();
|
|
11468
|
-
if (!connectionString) fail("A connection string is required for bring-your-own databases.", "Pass `--connection-string <url>`.", "input_required");
|
|
11469
|
-
}
|
|
11470
|
-
try {
|
|
11471
|
-
const created = await client.data.collection("databases").create({
|
|
11472
|
-
project: projectId,
|
|
11473
|
-
type,
|
|
11474
|
-
connectionString: type === "byodb" ? connectionString : void 0,
|
|
11475
|
-
connectionStatus: "untested"
|
|
11476
|
-
});
|
|
11477
|
-
success(`Attached ${type} database to project ${projectRef}`);
|
|
11478
|
-
emit(() => {
|
|
11479
|
-
keyValues([["ID", String(created.id)]]);
|
|
11480
|
-
if (type === "byodb") {
|
|
11481
|
-
note(chalk.gray("Verify it with `rebase cloud db test`."));
|
|
11482
|
-
noteBlank();
|
|
12143
|
+
if (!quiet) {
|
|
12144
|
+
console.log("");
|
|
12145
|
+
console.log(chalk.bold.green(" ✓ Deployment succeeded"));
|
|
12146
|
+
console.log("");
|
|
11483
12147
|
}
|
|
11484
|
-
|
|
11485
|
-
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
} catch (e) {
|
|
11492
|
-
reportError(e, "Failed to attach database");
|
|
11493
|
-
}
|
|
11494
|
-
}
|
|
11495
|
-
async function testDatabase(rawArgs) {
|
|
11496
|
-
const { client } = await requireClient(rawArgs);
|
|
11497
|
-
const projectId = await requireProject(rawArgs, client);
|
|
11498
|
-
const projectRef = displayProjectRef(rawArgs);
|
|
11499
|
-
noteBlank();
|
|
11500
|
-
note(`Testing database connectivity for project ${chalk.bold(projectRef)}...`);
|
|
11501
|
-
try {
|
|
11502
|
-
const res = await client.functions.invoke("db-test", { projectId });
|
|
11503
|
-
if (res.logs) console.error(`\n${res.logs}`);
|
|
11504
|
-
if (!res.success) fail("Database connection failed.", "The connection log above (stderr) has the reason.", "db_connection_failed");
|
|
11505
|
-
success("Database connection succeeded");
|
|
11506
|
-
emit(() => {}, {
|
|
11507
|
-
success: true,
|
|
11508
|
-
projectId,
|
|
11509
|
-
logs: res.logs ?? null
|
|
11510
|
-
});
|
|
11511
|
-
} catch (e) {
|
|
11512
|
-
reportError(e, "Failed to test database");
|
|
12148
|
+
return dep.status;
|
|
12149
|
+
}
|
|
12150
|
+
if (Date.now() - started > timeoutMs) {
|
|
12151
|
+
if (!quiet) console.log("");
|
|
12152
|
+
fail(`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for the build to finish.`, "The deployment may still be running — check `rebase cloud logs`.", "timeout");
|
|
12153
|
+
}
|
|
12154
|
+
await sleep(POLL_INTERVAL_MS);
|
|
11513
12155
|
}
|
|
11514
12156
|
}
|
|
11515
|
-
|
|
11516
|
-
* `rebase cloud db info [--reveal]` — where a project's database actually lives.
|
|
11517
|
-
*
|
|
11518
|
-
* The password is NEVER in the default output; `--reveal` fetches it through the
|
|
11519
|
-
* separate reveal call, and it appears in JSON only when `--reveal` is given.
|
|
11520
|
-
* Any field the server could not resolve comes back `null` and is rendered as
|
|
11521
|
-
* unavailable, never a placeholder.
|
|
11522
|
-
*/
|
|
11523
|
-
async function dbInfo(rawArgs) {
|
|
12157
|
+
async function logsCommand(rawArgs, projectRef) {
|
|
11524
12158
|
const args = arg({
|
|
11525
|
-
"--
|
|
11526
|
-
"--
|
|
11527
|
-
"-
|
|
12159
|
+
"--runtime": Boolean,
|
|
12160
|
+
"--follow": Boolean,
|
|
12161
|
+
"-f": "--follow"
|
|
11528
12162
|
}, {
|
|
11529
12163
|
argv: rawArgs.slice(2),
|
|
11530
12164
|
permissive: true
|
|
11531
12165
|
});
|
|
11532
12166
|
const { client } = await requireClient(rawArgs);
|
|
11533
|
-
const projectId = await
|
|
11534
|
-
|
|
11535
|
-
|
|
11536
|
-
|
|
11537
|
-
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
|
|
11541
|
-
|
|
11542
|
-
|
|
11543
|
-
|
|
11544
|
-
|
|
11545
|
-
|
|
11546
|
-
|
|
12167
|
+
const projectId = await resolveProjectRef(projectRef, client);
|
|
12168
|
+
if (args["--runtime"]) {
|
|
12169
|
+
try {
|
|
12170
|
+
const res = await client.functions.invoke("runtime-logs", void 0, {
|
|
12171
|
+
method: "GET",
|
|
12172
|
+
path: projectId
|
|
12173
|
+
});
|
|
12174
|
+
console.log("");
|
|
12175
|
+
console.log(chalk.bold(` 📄 Runtime logs — project ${projectRef}`));
|
|
12176
|
+
console.log("");
|
|
12177
|
+
console.log(res.logs ?? chalk.gray(" (no logs)"));
|
|
12178
|
+
console.log("");
|
|
12179
|
+
} catch (e) {
|
|
12180
|
+
reportError(e, "Failed to fetch runtime logs");
|
|
11547
12181
|
}
|
|
11548
|
-
|
|
12182
|
+
return;
|
|
12183
|
+
}
|
|
12184
|
+
try {
|
|
12185
|
+
const dep = await latestDeployment(client, projectId);
|
|
12186
|
+
if (!dep) {
|
|
11549
12187
|
console.log("");
|
|
11550
|
-
console.log(chalk.
|
|
12188
|
+
console.log(chalk.gray(" No deployments yet for this project."));
|
|
11551
12189
|
console.log("");
|
|
11552
|
-
|
|
11553
|
-
|
|
11554
|
-
|
|
11555
|
-
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
11559
|
-
|
|
11560
|
-
if (info.unavailableReason) console.log(chalk.gray(` ${info.unavailableReason}`));
|
|
11561
|
-
if (info.portForward) {
|
|
11562
|
-
const pf = info.portForward;
|
|
11563
|
-
console.log("");
|
|
11564
|
-
console.log(chalk.gray(` Port-forward: kubectl -n ${pf.namespace} port-forward svc/${pf.service} ${pf.localPort}:${pf.remotePort}`));
|
|
11565
|
-
}
|
|
12190
|
+
return;
|
|
12191
|
+
}
|
|
12192
|
+
console.log("");
|
|
12193
|
+
console.log(chalk.bold(` 📄 Build logs — deployment ${dep.id}`) + ` ${colorStatus(dep.status)}`);
|
|
12194
|
+
console.log("");
|
|
12195
|
+
if (args["--follow"] && dep.status === "deploying") await streamBuildLogs(client, String(dep.id));
|
|
12196
|
+
else {
|
|
12197
|
+
console.log(dep.logs ?? chalk.gray(" (no logs)"));
|
|
11566
12198
|
console.log("");
|
|
11567
|
-
}
|
|
11568
|
-
projectId,
|
|
11569
|
-
type: info.type,
|
|
11570
|
-
host: info.host,
|
|
11571
|
-
port: info.port,
|
|
11572
|
-
database: info.database,
|
|
11573
|
-
username: info.username,
|
|
11574
|
-
passwordAvailable: info.passwordAvailable,
|
|
11575
|
-
portForward: info.portForward,
|
|
11576
|
-
unavailableReason: info.unavailableReason,
|
|
11577
|
-
...args["--reveal"] ? {
|
|
11578
|
-
password,
|
|
11579
|
-
connectionString
|
|
11580
|
-
} : {}
|
|
11581
|
-
});
|
|
12199
|
+
}
|
|
11582
12200
|
} catch (e) {
|
|
11583
|
-
reportError(e, "Failed to
|
|
12201
|
+
reportError(e, "Failed to fetch build logs");
|
|
11584
12202
|
}
|
|
11585
12203
|
}
|
|
12204
|
+
//#endregion
|
|
12205
|
+
//#region src/commands/cloud/orgs.ts
|
|
11586
12206
|
/**
|
|
11587
|
-
* `
|
|
11588
|
-
*
|
|
11589
|
-
* Both halves were reachable by the old operand filter, and both are
|
|
11590
|
-
* destructive: `rebase cloud db backup -p acme` read `--project`'s value as the
|
|
11591
|
-
* ACTION (falling through to a list, so the flag silently changed what ran),
|
|
11592
|
-
* and `db backup restore -p acme` read it as the FILENAME — a restore staged
|
|
11593
|
-
* over the live database, named after the project slug. An undeclared flag was
|
|
11594
|
-
* dropped instead of refused, which is the same failure one step quieter: `db
|
|
11595
|
-
* backup --dry-run` ran a list, having silently discarded the flag that was
|
|
11596
|
-
* supposed to change what it did.
|
|
11597
|
-
*
|
|
11598
|
-
* Exported so its tests drive the real parser.
|
|
12207
|
+
* `rebase cloud orgs` — list / create / members.
|
|
11599
12208
|
*/
|
|
11600
|
-
function
|
|
11601
|
-
|
|
11602
|
-
|
|
11603
|
-
|
|
11604
|
-
|
|
11605
|
-
|
|
11606
|
-
|
|
11607
|
-
|
|
11608
|
-
|
|
11609
|
-
|
|
11610
|
-
|
|
11611
|
-
|
|
11612
|
-
|
|
12209
|
+
async function orgsCommand(subcommand, rawArgs) {
|
|
12210
|
+
switch (subcommand) {
|
|
12211
|
+
case "list":
|
|
12212
|
+
case void 0:
|
|
12213
|
+
await listOrgs(rawArgs);
|
|
12214
|
+
break;
|
|
12215
|
+
case "create":
|
|
12216
|
+
await createOrg(rawArgs);
|
|
12217
|
+
break;
|
|
12218
|
+
case "members":
|
|
12219
|
+
await listMembers(rawArgs);
|
|
12220
|
+
break;
|
|
12221
|
+
case "--help":
|
|
12222
|
+
printOrgsHelp();
|
|
12223
|
+
break;
|
|
12224
|
+
default: fail(`Unknown orgs command: ${subcommand}`, "Run `rebase cloud orgs --help`.", "unknown_command");
|
|
12225
|
+
}
|
|
11613
12226
|
}
|
|
11614
|
-
async function
|
|
11615
|
-
const {
|
|
11616
|
-
const { client } = await requireClient(rawArgs);
|
|
11617
|
-
const projectId = await requireProject(rawArgs, client);
|
|
11618
|
-
const projectRef = displayProjectRef(rawArgs);
|
|
12227
|
+
async function listOrgs(rawArgs) {
|
|
12228
|
+
const { client, url } = await requireClient(rawArgs);
|
|
11619
12229
|
try {
|
|
11620
|
-
|
|
11621
|
-
|
|
11622
|
-
projectId,
|
|
11623
|
-
type: "manual"
|
|
11624
|
-
}, { path: "create" });
|
|
11625
|
-
if (!res.success) fail(res.error || "Backup failed.");
|
|
11626
|
-
emit(() => success(`Backup created: ${res.backup?.filename ?? "(unknown)"}`), {
|
|
11627
|
-
success: true,
|
|
11628
|
-
backup: res.backup ?? null
|
|
11629
|
-
});
|
|
11630
|
-
return;
|
|
11631
|
-
}
|
|
11632
|
-
if (action === "restore") {
|
|
11633
|
-
const filename = backupFile;
|
|
11634
|
-
if (!filename) fail("Usage: rebase cloud db backup restore <filename>", void 0, "usage");
|
|
11635
|
-
await confirmDestructive({
|
|
11636
|
-
yes: Boolean(args["--yes"]),
|
|
11637
|
-
prompt: `Restore "${filename}" over the current database for project ${projectRef}?`
|
|
11638
|
-
});
|
|
11639
|
-
const res = await client.functions.invoke("backup", {
|
|
11640
|
-
projectId,
|
|
11641
|
-
filename
|
|
11642
|
-
}, { path: "restore" });
|
|
11643
|
-
if (!res.success) fail(res.error || "Restore failed.");
|
|
11644
|
-
emit(() => success(res.message || "Restore complete"), {
|
|
11645
|
-
success: true,
|
|
11646
|
-
message: res.message ?? null
|
|
11647
|
-
});
|
|
11648
|
-
return;
|
|
11649
|
-
}
|
|
11650
|
-
if (action === "status") {
|
|
11651
|
-
const res = await client.functions.invoke("backup", void 0, {
|
|
11652
|
-
method: "GET",
|
|
11653
|
-
path: `backup-status/${projectId}`
|
|
11654
|
-
});
|
|
11655
|
-
emit(() => {
|
|
11656
|
-
console.log("");
|
|
11657
|
-
console.log(chalk.bold(` 💾 Automated backups — project ${projectRef}`));
|
|
11658
|
-
console.log("");
|
|
11659
|
-
keyValues([
|
|
11660
|
-
["Enabled", res.enabled ? chalk.green("yes") : chalk.yellow("no")],
|
|
11661
|
-
["Reason", String(res.reason ?? "")],
|
|
11662
|
-
["Database type", String(res.databaseType ?? "")],
|
|
11663
|
-
["Last backup", res.lastSuccessfulBackup ?? void 0],
|
|
11664
|
-
["Recovery window", res.recoveryWindow ? `${res.recoveryWindow.from} → ${res.recoveryWindow.to}` : void 0]
|
|
11665
|
-
]);
|
|
11666
|
-
console.log("");
|
|
11667
|
-
}, res);
|
|
11668
|
-
return;
|
|
11669
|
-
}
|
|
11670
|
-
if (action === "download") {
|
|
11671
|
-
const filename = backupFile;
|
|
11672
|
-
if (!filename) fail("Usage: rebase cloud db backup download <filename>", void 0, "usage");
|
|
11673
|
-
const res = await client.functions.invoke("backup", void 0, {
|
|
11674
|
-
method: "GET",
|
|
11675
|
-
path: `download/${projectId}/${encodeURIComponent(filename)}`
|
|
11676
|
-
});
|
|
11677
|
-
emit(() => {
|
|
11678
|
-
console.log("");
|
|
11679
|
-
console.log(chalk.bold(` ${res.name}`) + chalk.gray(` ${(res.size / 1024 / 1024).toFixed(1)} MB`));
|
|
11680
|
-
console.log(` ${chalk.cyan(res.url)}`);
|
|
11681
|
-
console.log("");
|
|
11682
|
-
console.log(chalk.gray(" Short-lived signed URL — fetch it with curl/wget."));
|
|
11683
|
-
console.log("");
|
|
11684
|
-
}, {
|
|
11685
|
-
name: res.name,
|
|
11686
|
-
size: res.size,
|
|
11687
|
-
url: res.url
|
|
11688
|
-
});
|
|
11689
|
-
return;
|
|
11690
|
-
}
|
|
11691
|
-
const res = await client.functions.invoke("backup", void 0, {
|
|
11692
|
-
method: "GET",
|
|
11693
|
-
path: `list/${projectId}`
|
|
11694
|
-
});
|
|
12230
|
+
const orgs = (await client.data.collection("organizations").find({ limit: 100 })).data;
|
|
12231
|
+
const active = getContextOrg(url);
|
|
11695
12232
|
emit(() => {
|
|
11696
12233
|
console.log("");
|
|
11697
|
-
console.log(chalk.bold(
|
|
12234
|
+
console.log(chalk.bold(" 🏢 Organizations"));
|
|
11698
12235
|
console.log("");
|
|
11699
|
-
if (
|
|
11700
|
-
console.log(chalk.gray("
|
|
12236
|
+
if (orgs.length === 0) {
|
|
12237
|
+
console.log(chalk.gray(" You are not a member of any organization."));
|
|
11701
12238
|
console.log("");
|
|
11702
12239
|
return;
|
|
11703
12240
|
}
|
|
11704
|
-
for (const
|
|
11705
|
-
const
|
|
11706
|
-
console.log(
|
|
12241
|
+
for (const o of orgs) {
|
|
12242
|
+
const marker = String(o.id) === active ? chalk.green(" ●") : " ";
|
|
12243
|
+
console.log(`${marker}${chalk.bold(o.name ?? "(unnamed)")} ${chalk.gray(`[${o.id}]`)}${o.slug ? chalk.gray(` ${o.slug}`) : ""}`);
|
|
11707
12244
|
}
|
|
11708
12245
|
console.log("");
|
|
12246
|
+
note(chalk.gray("● = active organization. Switch with `rebase cloud use <id>`."));
|
|
12247
|
+
console.log("");
|
|
11709
12248
|
}, {
|
|
11710
|
-
|
|
11711
|
-
|
|
12249
|
+
activeOrg: active ?? null,
|
|
12250
|
+
organizations: orgs.map((o) => ({
|
|
12251
|
+
id: String(o.id),
|
|
12252
|
+
name: o.name ?? null,
|
|
12253
|
+
slug: o.slug ?? null,
|
|
12254
|
+
active: String(o.id) === active
|
|
12255
|
+
}))
|
|
11712
12256
|
});
|
|
11713
12257
|
} catch (e) {
|
|
11714
|
-
reportError(e, "
|
|
12258
|
+
reportError(e, "Failed to list organizations");
|
|
11715
12259
|
}
|
|
11716
12260
|
}
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
* over to (it is now the live database). Every mutating step requires `--yes` in
|
|
11726
|
-
* non-interactive use, and the CLI surfaces these staged semantics honestly.
|
|
11727
|
-
*/
|
|
11728
|
-
async function pitrCommand(rawArgs) {
|
|
11729
|
-
const { flags: args, positionals } = parseCloudArgs({
|
|
11730
|
-
spec: {
|
|
11731
|
-
"--target": String,
|
|
11732
|
-
"--yes": Boolean
|
|
11733
|
-
},
|
|
11734
|
-
rawArgs,
|
|
11735
|
-
commandWords: 3,
|
|
11736
|
-
command: "cloud db pitr",
|
|
11737
|
-
maxPositionals: 1
|
|
12261
|
+
async function createOrg(rawArgs) {
|
|
12262
|
+
const args = arg({
|
|
12263
|
+
"--name": String,
|
|
12264
|
+
"--slug": String,
|
|
12265
|
+
"-n": "--name"
|
|
12266
|
+
}, {
|
|
12267
|
+
argv: rawArgs.slice(4),
|
|
12268
|
+
permissive: true
|
|
11738
12269
|
});
|
|
11739
|
-
const
|
|
11740
|
-
const
|
|
11741
|
-
|
|
11742
|
-
|
|
12270
|
+
const { client, url } = await requireClient(rawArgs);
|
|
12271
|
+
const prompts = [];
|
|
12272
|
+
if (!args["--name"]) {
|
|
12273
|
+
requireInteractive("an organization name", "--name <name>");
|
|
12274
|
+
prompts.push({
|
|
12275
|
+
type: "input",
|
|
12276
|
+
name: "name",
|
|
12277
|
+
message: "Organization name:"
|
|
12278
|
+
});
|
|
12279
|
+
}
|
|
12280
|
+
const answers = prompts.length ? await inquirer.prompt(prompts) : {};
|
|
12281
|
+
const name = (args["--name"] || answers.name || "").trim();
|
|
12282
|
+
if (!name) fail("Organization name is required.", "Pass `--name <name>`.", "input_required");
|
|
12283
|
+
const slug = (args["--slug"] || slugify(name)).trim();
|
|
11743
12284
|
try {
|
|
11744
|
-
|
|
11745
|
-
|
|
11746
|
-
|
|
11747
|
-
|
|
11748
|
-
|
|
11749
|
-
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11753
|
-
|
|
11754
|
-
|
|
11755
|
-
|
|
11756
|
-
|
|
11757
|
-
|
|
11758
|
-
|
|
11759
|
-
|
|
11760
|
-
|
|
11761
|
-
|
|
11762
|
-
|
|
11763
|
-
|
|
11764
|
-
|
|
11765
|
-
|
|
11766
|
-
|
|
11767
|
-
|
|
11768
|
-
|
|
11769
|
-
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
11773
|
-
|
|
11774
|
-
|
|
11775
|
-
|
|
11776
|
-
console.log("");
|
|
11777
|
-
console.log(chalk.yellow(` ⏳ ${String(res.message ?? "Recovery staged.")}`));
|
|
11778
|
-
console.log(chalk.gray(" Watch progress with `rebase cloud db pitr status`, then `rebase cloud db pitr cutover --yes`."));
|
|
11779
|
-
console.log("");
|
|
11780
|
-
}, res);
|
|
11781
|
-
return;
|
|
11782
|
-
}
|
|
11783
|
-
if (action === "cutover") {
|
|
11784
|
-
await confirmDestructive({
|
|
11785
|
-
yes: Boolean(args["--yes"]),
|
|
11786
|
-
prompt: `Cut project ${projectRef} over to the staged recovery? This repoints and restarts your application.`
|
|
11787
|
-
});
|
|
11788
|
-
const res = await client.functions.invoke("backup", { projectId }, { path: "pitr-restore-cutover" });
|
|
11789
|
-
emit(() => {
|
|
11790
|
-
console.log("");
|
|
11791
|
-
console.log(String(res.message ?? "Cutover requested."));
|
|
12285
|
+
const created = await client.data.collection("organizations").create({
|
|
12286
|
+
name,
|
|
12287
|
+
slug,
|
|
12288
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12289
|
+
});
|
|
12290
|
+
setContextOrg(url, String(created.id));
|
|
12291
|
+
success(`Created organization ${chalk.bold(name)} and set it active`);
|
|
12292
|
+
emit(() => {}, {
|
|
12293
|
+
success: true,
|
|
12294
|
+
id: String(created.id),
|
|
12295
|
+
name,
|
|
12296
|
+
slug,
|
|
12297
|
+
setActive: true
|
|
12298
|
+
});
|
|
12299
|
+
} catch (e) {
|
|
12300
|
+
reportError(e, "Failed to create organization");
|
|
12301
|
+
}
|
|
12302
|
+
}
|
|
12303
|
+
async function listMembers(rawArgs) {
|
|
12304
|
+
const { client, url } = await requireClient(rawArgs);
|
|
12305
|
+
const org = getContextOrg(url);
|
|
12306
|
+
if (!org) fail("No active organization.", "Run `rebase cloud use` first.", "no_org");
|
|
12307
|
+
try {
|
|
12308
|
+
const members = (await client.data.collection("organization-members").find({
|
|
12309
|
+
where: { organization: ["==", org] },
|
|
12310
|
+
limit: 200
|
|
12311
|
+
})).data;
|
|
12312
|
+
emit(() => {
|
|
12313
|
+
console.log("");
|
|
12314
|
+
console.log(chalk.bold(` 👥 Members — org ${org}`));
|
|
12315
|
+
console.log("");
|
|
12316
|
+
if (members.length === 0) {
|
|
12317
|
+
console.log(chalk.gray(" No members found."));
|
|
11792
12318
|
console.log("");
|
|
11793
|
-
|
|
11794
|
-
|
|
11795
|
-
|
|
11796
|
-
|
|
11797
|
-
|
|
11798
|
-
|
|
11799
|
-
|
|
11800
|
-
|
|
11801
|
-
|
|
11802
|
-
|
|
11803
|
-
|
|
11804
|
-
}
|
|
11805
|
-
fail(`Unknown pitr command: ${action}`, "Try status | restore | cutover | discard.", "usage");
|
|
12319
|
+
return;
|
|
12320
|
+
}
|
|
12321
|
+
for (const m of members) console.log(` ${chalk.bold(m.userId ?? "?")} ${colorStatus(m.role)}`);
|
|
12322
|
+
console.log("");
|
|
12323
|
+
}, {
|
|
12324
|
+
org,
|
|
12325
|
+
members: members.map((m) => ({
|
|
12326
|
+
id: String(m.id),
|
|
12327
|
+
userId: m.userId ?? null,
|
|
12328
|
+
role: m.role ?? null
|
|
12329
|
+
}))
|
|
12330
|
+
});
|
|
11806
12331
|
} catch (e) {
|
|
11807
|
-
reportError(e, "
|
|
12332
|
+
reportError(e, "Failed to list members");
|
|
11808
12333
|
}
|
|
11809
12334
|
}
|
|
11810
|
-
function
|
|
11811
|
-
|
|
12335
|
+
function slugify(s) {
|
|
12336
|
+
return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
12337
|
+
}
|
|
12338
|
+
function printOrgsHelp() {
|
|
12339
|
+
emitHelp("orgs", [
|
|
11812
12340
|
"list",
|
|
11813
12341
|
"create",
|
|
11814
|
-
"
|
|
11815
|
-
"test",
|
|
11816
|
-
"backup",
|
|
11817
|
-
"pitr"
|
|
12342
|
+
"members"
|
|
11818
12343
|
], () => {
|
|
11819
12344
|
console.log(`
|
|
11820
|
-
${chalk.bold("rebase cloud
|
|
12345
|
+
${chalk.bold("rebase cloud orgs")} — Manage organizations
|
|
11821
12346
|
|
|
11822
12347
|
${chalk.green.bold("Commands")}
|
|
11823
|
-
${chalk.blue.bold("list")}
|
|
11824
|
-
${chalk.blue.bold("create")}
|
|
11825
|
-
${chalk.blue.bold("
|
|
11826
|
-
${chalk.blue.bold("test")} Test database connectivity
|
|
11827
|
-
${chalk.blue.bold("backup list")} List backups
|
|
11828
|
-
${chalk.blue.bold("backup create")} Create a manual backup
|
|
11829
|
-
${chalk.blue.bold("backup restore")} ${chalk.gray("<file>")} Restore a backup
|
|
11830
|
-
${chalk.blue.bold("backup status")} Automated-backup health
|
|
11831
|
-
${chalk.blue.bold("backup download")} ${chalk.gray("<file>")} Signed URL for a backup
|
|
11832
|
-
${chalk.blue.bold("pitr status")} Point-in-time recovery window
|
|
11833
|
-
${chalk.blue.bold("pitr restore")} ${chalk.gray("--target <ISO>")} Stage a recovery ${chalk.gray("(does not repoint)")}
|
|
11834
|
-
${chalk.blue.bold("pitr cutover")} ${chalk.gray("-y")} Repoint the app at the staged recovery
|
|
11835
|
-
${chalk.blue.bold("pitr discard")} ${chalk.gray("-y")} Delete a staged recovery
|
|
11836
|
-
|
|
11837
|
-
${chalk.green.bold("Options")}
|
|
11838
|
-
${chalk.blue("--project, -p")} Project slug ${chalk.gray("(defaults to the linked project)")}
|
|
11839
|
-
${chalk.blue("--reveal")} Include the DB password ${chalk.gray("(info)")}
|
|
11840
|
-
${chalk.blue("--type")} managed | byodb ${chalk.gray("(create)")}
|
|
11841
|
-
${chalk.blue("--connection-string")} External DB URL ${chalk.gray("(byodb)")}
|
|
11842
|
-
${chalk.blue("--json")} Machine-readable output
|
|
12348
|
+
${chalk.blue.bold("list")} List organizations you belong to
|
|
12349
|
+
${chalk.blue.bold("create")} Create a new organization ${chalk.gray("(--name, --slug)")}
|
|
12350
|
+
${chalk.blue.bold("members")} List members of the active organization
|
|
11843
12351
|
`);
|
|
11844
12352
|
});
|
|
11845
12353
|
}
|
|
@@ -13832,6 +14340,208 @@ ${chalk.gray("so it works in a deploy script. To restart a workload, use `rebase
|
|
|
13832
14340
|
});
|
|
13833
14341
|
}
|
|
13834
14342
|
//#endregion
|
|
14343
|
+
//#region src/commands/cloud/action-help.ts
|
|
14344
|
+
/**
|
|
14345
|
+
* Per-action help for `rebase cloud`.
|
|
14346
|
+
*
|
|
14347
|
+
* `--help` was already answered centrally, but only per *group*: `rebase cloud
|
|
14348
|
+
* projects create --help` and `rebase cloud deploy --help` both fell through to
|
|
14349
|
+
* the index page, which lists groups and no flags at all. So the flags that
|
|
14350
|
+
* decide what those commands do — `--name`, `--subdomain`, `--type`, `--bundle`
|
|
14351
|
+
* — had no discoverable spelling anywhere. The way they were found on a real
|
|
14352
|
+
* first deploy was by reading `dist/index.es.js.map`. An agent cannot do that,
|
|
14353
|
+
* and neither should a person.
|
|
14354
|
+
*
|
|
14355
|
+
* Two rules hold this file honest:
|
|
14356
|
+
*
|
|
14357
|
+
* 1. **The spec is the source of truth, not this page.** `action-help.test.ts`
|
|
14358
|
+
* pairs every entry with the `arg` spec its command actually parses and
|
|
14359
|
+
* asserts they agree in both directions. A flag added to a command without
|
|
14360
|
+
* a line here is a failing test, not a page that is quietly a year old.
|
|
14361
|
+
* The pairing lives in the test rather than here on purpose: importing the
|
|
14362
|
+
* command modules to describe them would make `--help` depend on the code
|
|
14363
|
+
* it is meant to be readable without.
|
|
14364
|
+
* 2. **It answers in the reader's language.** Like every other page in this
|
|
14365
|
+
* family it goes through `emitHelp`, so a piped `--help` is a structured
|
|
14366
|
+
* description of the command rather than sixty lines of ANSI to scrape.
|
|
14367
|
+
*/
|
|
14368
|
+
/**
|
|
14369
|
+
* Flags every cloud command accepts, documented once.
|
|
14370
|
+
*
|
|
14371
|
+
* Excluded from the spec comparison below — they are merged in by
|
|
14372
|
+
* `parseCloudArgs` for every command in the family, so repeating them per entry
|
|
14373
|
+
* would be nine copies of the same four lines.
|
|
14374
|
+
*/
|
|
14375
|
+
var GLOBAL_HELP_FLAGS = [
|
|
14376
|
+
["--project, -p <slug>", "Operate on a project without linking this directory"],
|
|
14377
|
+
["--json", "Machine-readable output (also when piped, or REBASE_JSON=1)"],
|
|
14378
|
+
["--url <origin>", "Target a specific control plane (or REBASE_CLOUD_URL)"],
|
|
14379
|
+
["--yes, -y", "Skip confirmation prompts"],
|
|
14380
|
+
["--debug", "Print the untouched error body after a failure"]
|
|
14381
|
+
];
|
|
14382
|
+
var ACTION_HELP = {
|
|
14383
|
+
projects: {
|
|
14384
|
+
command: "cloud projects",
|
|
14385
|
+
usage: "cloud projects <list|create|info|delete> [options]",
|
|
14386
|
+
summary: "List, create, inspect and delete projects in the selected organization.",
|
|
14387
|
+
flags: [],
|
|
14388
|
+
examples: [
|
|
14389
|
+
"rebase cloud projects list",
|
|
14390
|
+
"rebase cloud projects create --help",
|
|
14391
|
+
"rebase cloud projects info shop",
|
|
14392
|
+
"rebase cloud projects delete shop --yes"
|
|
14393
|
+
]
|
|
14394
|
+
},
|
|
14395
|
+
clusters: {
|
|
14396
|
+
command: "cloud clusters",
|
|
14397
|
+
usage: "cloud clusters [list|add|verify] [options]",
|
|
14398
|
+
summary: "The compute clusters tenants run on. Platform-admin only.",
|
|
14399
|
+
flags: [],
|
|
14400
|
+
examples: [
|
|
14401
|
+
"rebase cloud clusters",
|
|
14402
|
+
"rebase cloud clusters verify <cluster-id> --baseline",
|
|
14403
|
+
"rebase cloud clusters add --name gke-eu --provider gcp --region europe-west1 --kubeconfig ./kubeconfig"
|
|
14404
|
+
]
|
|
14405
|
+
},
|
|
14406
|
+
"projects create": {
|
|
14407
|
+
command: "cloud projects create",
|
|
14408
|
+
usage: "cloud projects create --name <name> --subdomain <slug> [options]",
|
|
14409
|
+
summary: "Create a project. Unless --db says otherwise a managed database is attached in the same call, because a project without one can never deploy — it sits at status \"provisioning\" indefinitely, and that word does not mean work is underway.",
|
|
14410
|
+
flags: [
|
|
14411
|
+
["--name, -n <name>", "Display name. Required (prompted only on a terminal)"],
|
|
14412
|
+
["--subdomain <slug>", "The <slug>.rebase.website host. Required, and not editable afterwards"],
|
|
14413
|
+
["--db <managed|byodb|none>", "Which database to attach. Default: managed"],
|
|
14414
|
+
["--connection-string <url>", "The PostgreSQL URL, for --db byodb"],
|
|
14415
|
+
["--org <id>", "Organization to create it in (default: the selected one)"],
|
|
14416
|
+
["--link", "Link this directory to the new project"],
|
|
14417
|
+
["--repo <url>", "Git repository to build from"],
|
|
14418
|
+
["--branch <name>", "Git branch. Default: main"],
|
|
14419
|
+
["--provider <gcp|hetzner|…>", "Where to run it (default: the platform's target)"],
|
|
14420
|
+
["--region <region>", "Region within the provider"],
|
|
14421
|
+
["--cpu <n>", "vCPU per instance"],
|
|
14422
|
+
["--memory <size>", "Memory per instance, e.g. 512Mi"],
|
|
14423
|
+
["--replicas <n>", "Instance count"],
|
|
14424
|
+
["--spot <true|false>", "Run on preemptible capacity"],
|
|
14425
|
+
["--scale-to-zero <true|false>", "Stop the instances when idle"],
|
|
14426
|
+
["--db-mode <mode>", "Database topology dial"],
|
|
14427
|
+
["--db-instances <n>", "Database instance count"],
|
|
14428
|
+
["--db-cpu <n>", "vCPU per database instance"],
|
|
14429
|
+
["--db-memory <size>", "Memory per database instance"],
|
|
14430
|
+
["--storage <size>", "Database volume size"]
|
|
14431
|
+
],
|
|
14432
|
+
examples: [
|
|
14433
|
+
"rebase cloud projects create --name \"Shop\" --subdomain shop --link",
|
|
14434
|
+
"rebase cloud projects create --name Shop --subdomain shop --db none",
|
|
14435
|
+
"rebase cloud projects create --name Shop --subdomain shop --db byodb --connection-string \"$DATABASE_URL\""
|
|
14436
|
+
],
|
|
14437
|
+
notes: ["The subdomain cannot be changed in passing later — a typo here is a new project.", "A managed database is created at the project's FIRST DEPLOY, not here."]
|
|
14438
|
+
},
|
|
14439
|
+
"db create": {
|
|
14440
|
+
command: "cloud db create",
|
|
14441
|
+
usage: "cloud db create --type <managed|byodb> [options]",
|
|
14442
|
+
summary: "Attach a database to the project. Required before the first deploy: a project with no database stays at status \"provisioning\" forever, waiting for this command. `rebase cloud projects create` now does it for you unless you passed --db none.",
|
|
14443
|
+
flags: [
|
|
14444
|
+
["--type <managed|byodb>", "Platform-provisioned Postgres, or your own"],
|
|
14445
|
+
["--connection-string <url>", "The PostgreSQL URL, for --type byodb"],
|
|
14446
|
+
["--wait", "Wait for the database to answer (byodb only — see below)"],
|
|
14447
|
+
["--timeout <seconds>", "Ceiling on --wait. Default: 300"]
|
|
14448
|
+
],
|
|
14449
|
+
examples: ["rebase cloud db create --type managed", "rebase cloud db create --type byodb --connection-string \"$DATABASE_URL\" --wait"],
|
|
14450
|
+
notes: [
|
|
14451
|
+
"A managed database is CloudNativePG in a shared in-cluster pool, and it is created at the project's first deploy. There is nothing to poll before then, so --wait says so and returns rather than looping.",
|
|
14452
|
+
"`rebase cloud db test` legitimately fails before the first deploy.",
|
|
14453
|
+
"A project has exactly one database — attaching a second is refused, because the platform reads one row and it becomes undefined which it deploys against."
|
|
14454
|
+
]
|
|
14455
|
+
},
|
|
14456
|
+
deploy: {
|
|
14457
|
+
command: "cloud deploy",
|
|
14458
|
+
usage: "cloud deploy [app] [options]",
|
|
14459
|
+
summary: "Deploy an app of the linked project and follow the build to a terminal state, exiting non-zero if it failed. With no app named, the repository's backend is deployed.",
|
|
14460
|
+
flags: [
|
|
14461
|
+
["--wait", "Follow to a terminal state. Already the default; here to be explicit"],
|
|
14462
|
+
["--timeout <seconds>", "Ceiling on the follow. Default: 900"],
|
|
14463
|
+
["--no-follow", "Return as soon as the build is triggered"],
|
|
14464
|
+
["--message, -m <text>", "Label the release"],
|
|
14465
|
+
["--bundle", "Force a managed-bundle deploy (the default for runtime: managed)"],
|
|
14466
|
+
["--bundle-dir <path>", "Deploy a bundle that is already built"],
|
|
14467
|
+
["--source <path>", "Upload this directory and build a container image from it"],
|
|
14468
|
+
["--skip-type-check", "Compile without type checking, as `rebase build` does"],
|
|
14469
|
+
["--force", "Leave the managed runtime on purpose (ejects to a container image)"]
|
|
14470
|
+
],
|
|
14471
|
+
examples: [
|
|
14472
|
+
"rebase cloud deploy",
|
|
14473
|
+
"rebase cloud deploy web --message \"add search\"",
|
|
14474
|
+
"rebase cloud deploy --timeout 300 --json"
|
|
14475
|
+
],
|
|
14476
|
+
notes: ["A project whose rebase.json declares runtime: managed deploys a bundle without --bundle.", "--source on a managed project is refused: it would swap the project onto a container image."]
|
|
14477
|
+
},
|
|
14478
|
+
logs: {
|
|
14479
|
+
command: "cloud logs",
|
|
14480
|
+
usage: "cloud logs [--runtime] [--follow]",
|
|
14481
|
+
summary: "The latest build log, or the running container's log with --runtime.",
|
|
14482
|
+
flags: [["--runtime", "The running app's log instead of the build log"], ["--follow, -f", "Tail a build that is still running"]],
|
|
14483
|
+
examples: [
|
|
14484
|
+
"rebase cloud logs",
|
|
14485
|
+
"rebase cloud logs --runtime",
|
|
14486
|
+
"rebase cloud logs -f"
|
|
14487
|
+
]
|
|
14488
|
+
},
|
|
14489
|
+
status: {
|
|
14490
|
+
command: "cloud status",
|
|
14491
|
+
usage: "cloud status",
|
|
14492
|
+
summary: "One-glance project status: URL, last deploy, runtime, database, storage — and blockedOn/nextAction, which say whether the platform is working or waiting for you.",
|
|
14493
|
+
flags: [],
|
|
14494
|
+
examples: ["rebase cloud status", "rebase cloud status --project shop --json"],
|
|
14495
|
+
notes: ["Poll `status` only while blockedOn is null. Any other value names a command, and the state will not change until you run it."]
|
|
14496
|
+
},
|
|
14497
|
+
"clusters verify": {
|
|
14498
|
+
command: "cloud clusters verify",
|
|
14499
|
+
usage: "cloud clusters verify <cluster-id> [--baseline]",
|
|
14500
|
+
summary: "Ask a registered cluster whether it can host tenants. Reports permissions.allowed and permissions.denied, which is what names a missing RBAC grant.",
|
|
14501
|
+
flags: [["--baseline", "Also check ingress-nginx, cert-manager and CloudNativePG"]],
|
|
14502
|
+
examples: ["rebase cloud clusters verify gke-europe-west1 --baseline"],
|
|
14503
|
+
notes: ["Exits non-zero when the verdict is `unusable`, so it works as a gate."]
|
|
14504
|
+
}
|
|
14505
|
+
};
|
|
14506
|
+
/** Print one action's page — human, or its JSON description when piped. */
|
|
14507
|
+
function printActionHelp(entry) {
|
|
14508
|
+
emitHelp(entry.command, [], () => {
|
|
14509
|
+
console.log("");
|
|
14510
|
+
console.log(`${chalk.bold(`rebase ${entry.command}`)}`);
|
|
14511
|
+
console.log("");
|
|
14512
|
+
console.log(` ${entry.summary}`);
|
|
14513
|
+
console.log("");
|
|
14514
|
+
console.log(chalk.green.bold("Usage"));
|
|
14515
|
+
console.log(` rebase ${chalk.blue(entry.usage.replace(/^cloud /, "cloud "))}`);
|
|
14516
|
+
if (entry.flags.length > 0) {
|
|
14517
|
+
console.log("");
|
|
14518
|
+
console.log(chalk.green.bold("Options"));
|
|
14519
|
+
for (const [flag, description] of entry.flags) console.log(` ${chalk.blue(flag.padEnd(30))} ${description}`);
|
|
14520
|
+
}
|
|
14521
|
+
console.log("");
|
|
14522
|
+
console.log(chalk.green.bold("Global options"));
|
|
14523
|
+
for (const [flag, description] of GLOBAL_HELP_FLAGS) console.log(` ${chalk.blue(flag.padEnd(30))} ${chalk.gray(description)}`);
|
|
14524
|
+
if (entry.notes?.length) {
|
|
14525
|
+
console.log("");
|
|
14526
|
+
console.log(chalk.green.bold("Notes"));
|
|
14527
|
+
for (const note of entry.notes) console.log(` ${chalk.gray(`• ${note}`)}`);
|
|
14528
|
+
}
|
|
14529
|
+
console.log("");
|
|
14530
|
+
console.log(chalk.green.bold("Examples"));
|
|
14531
|
+
for (const example of entry.examples) console.log(` ${chalk.gray(example)}`);
|
|
14532
|
+
console.log("");
|
|
14533
|
+
}, {
|
|
14534
|
+
usage: `rebase ${entry.usage}`,
|
|
14535
|
+
summary: entry.summary,
|
|
14536
|
+
flags: [...entry.flags, ...GLOBAL_HELP_FLAGS].map(([flag, description]) => ({
|
|
14537
|
+
flag,
|
|
14538
|
+
description
|
|
14539
|
+
})),
|
|
14540
|
+
notes: entry.notes ?? [],
|
|
14541
|
+
examples: entry.examples
|
|
14542
|
+
});
|
|
14543
|
+
}
|
|
14544
|
+
//#endregion
|
|
13835
14545
|
//#region src/commands/cloud/index.ts
|
|
13836
14546
|
/**
|
|
13837
14547
|
* CLI command: `rebase cloud <group> [action] [options]`
|
|
@@ -13906,6 +14616,11 @@ async function cloudCommand(subcommand, rawArgs) {
|
|
|
13906
14616
|
return;
|
|
13907
14617
|
}
|
|
13908
14618
|
if (wantsHelp) {
|
|
14619
|
+
const page = (action ? ACTION_HELP[`${group} ${action}`] : void 0) ?? ACTION_HELP[group];
|
|
14620
|
+
if (page) {
|
|
14621
|
+
printActionHelp(page);
|
|
14622
|
+
return;
|
|
14623
|
+
}
|
|
13909
14624
|
(GROUP_HELP[group] ?? printCloudHelp)();
|
|
13910
14625
|
return;
|
|
13911
14626
|
}
|
|
@@ -14414,7 +15129,7 @@ async function entry(args) {
|
|
|
14414
15129
|
const effectiveSubcommand = parsedArgs["--help"] && !subcommand ? "--help" : subcommand;
|
|
14415
15130
|
switch (command) {
|
|
14416
15131
|
case "__dev-db-daemon": {
|
|
14417
|
-
const { parseDaemonArgs, runDaemon } = await import("./daemon-entry-
|
|
15132
|
+
const { parseDaemonArgs, runDaemon } = await import("./daemon-entry-CmJn83zu.js");
|
|
14418
15133
|
await runDaemon(parseDaemonArgs(args));
|
|
14419
15134
|
return;
|
|
14420
15135
|
}
|