@rebasepro/cli 0.17.3 → 0.18.1
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/LICENSE +0 -1
- package/README.md +21 -3
- package/bin/rebase.js +93 -6
- package/dist/branch-pointer-dBiClJ0p.js +111 -0
- package/dist/branch-pointer-dBiClJ0p.js.map +1 -0
- package/dist/bundle.d.ts +80 -3
- package/dist/cli.d.ts +22 -0
- package/dist/commands/auth.d.ts +13 -0
- package/dist/commands/cloud/action-help.d.ts +0 -10
- package/dist/commands/cloud/auth.d.ts +37 -0
- package/dist/commands/cloud/context.d.ts +90 -2
- package/dist/commands/cloud/databases.d.ts +0 -2
- package/dist/commands/cloud/declared-resources.d.ts +3 -0
- package/dist/commands/cloud/deploy.d.ts +76 -6
- package/dist/commands/cloud/deployments.d.ts +9 -2
- package/dist/commands/cloud/index.d.ts +36 -0
- package/dist/commands/cloud/link.d.ts +1 -1
- package/dist/commands/cloud/orgs.d.ts +6 -0
- package/dist/commands/cloud/resources.d.ts +81 -3
- package/dist/commands/cloud/settings.d.ts +7 -0
- package/dist/commands/db.d.ts +119 -9
- package/dist/commands/dev.d.ts +216 -1
- package/dist/commands/doctor.d.ts +51 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/skills.d.ts +82 -13
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/telemetry.d.ts +0 -8
- package/dist/{daemon-Bdl4lrdt.js → daemon-YDZK9NZ1.js} +88 -9
- package/dist/daemon-YDZK9NZ1.js.map +1 -0
- package/dist/{daemon-entry-CmJn83zu.js → daemon-entry-LTFKtpHy.js} +117 -44
- package/dist/daemon-entry-LTFKtpHy.js.map +1 -0
- package/dist/dev-db/branch-pointer.d.ts +71 -0
- package/dist/dev-db/daemon.d.ts +27 -0
- package/dist/dev-db/prepare.d.ts +62 -0
- package/dist/dev-db/pull.d.ts +28 -0
- package/dist/dev-db/resolve.d.ts +45 -6
- package/dist/dev-db/state.d.ts +21 -0
- package/dist/dev-preflight-CNLl4rdo.js +367 -0
- package/dist/dev-preflight-CNLl4rdo.js.map +1 -0
- package/dist/doctor-environment.d.ts +131 -0
- package/dist/function-portability.d.ts +7 -0
- package/dist/index.d.ts +25 -15
- package/dist/index.es.js +5594 -1741
- package/dist/index.es.js.map +1 -1
- package/dist/project-3WCbrrfW.js +499 -0
- package/dist/project-3WCbrrfW.js.map +1 -0
- package/dist/{pull-DqPRu1te.js → pull-Bj0XnH5s.js} +31 -2
- package/dist/pull-Bj0XnH5s.js.map +1 -0
- package/dist/resolve-Y56osuQH.js +74 -0
- package/dist/resolve-Y56osuQH.js.map +1 -0
- package/dist/resources/derive.d.ts +67 -1
- package/dist/resources/status.d.ts +128 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/{state-c0CJ6Kwb.js → state-C59Elrnt.js} +24 -15
- package/dist/state-C59Elrnt.js.map +1 -0
- package/dist/utils/args.d.ts +18 -0
- package/dist/utils/command-words.d.ts +45 -0
- package/dist/utils/dev-preflight.d.ts +46 -0
- package/dist/utils/package-manager.d.ts +23 -0
- package/dist/utils/project.d.ts +66 -0
- package/dist/utils/spawn-error.d.ts +1 -0
- package/dist/utils/unknown-command.d.ts +26 -0
- package/dist/utils/version.d.ts +10 -0
- package/package.json +36 -28
- package/templates/eject/backend/src/env.ts +9 -0
- package/templates/eject/backend/src/index.ts +46 -19
- package/templates/eject/config/resources.ts +1 -1
- package/templates/overlays/baas/README.md +31 -11
- package/templates/overlays/baas/backend/package.json +2 -2
- package/templates/overlays/baas/backend/tsconfig.json +1 -1
- package/templates/overlays/baas/package.json +5 -2
- package/templates/overlays/baas/pnpm-workspace.yaml +13 -0
- package/templates/template/.cursorrules +1 -0
- package/templates/template/.env.example +91 -8
- package/templates/template/.github/copilot-instructions.md +1 -0
- package/templates/template/.mcp.json +11 -0
- package/templates/template/.windsurfrules +1 -0
- package/templates/template/AGENTS.md +1 -0
- package/templates/template/CLAUDE.md +2 -1
- package/templates/template/README.md +59 -28
- package/templates/template/ai-instructions.md +29 -12
- package/templates/template/backend/functions/hello.ts +5 -1
- package/templates/template/backend/package.json +2 -2
- package/templates/template/backend/src/schema.generated.ts +8 -3
- package/templates/template/backend/tsconfig.json +1 -1
- package/templates/template/config/cms.d.ts +6 -0
- package/templates/template/config/package.json +1 -1
- package/templates/template/config/resources.ts +42 -3
- package/templates/template/config/storage.ts +2 -1
- package/templates/template/docker-compose.yml +50 -9
- package/templates/template/frontend/index.html +0 -1
- package/templates/template/frontend/package.json +1 -2
- package/templates/template/frontend/src/App.tsx +8 -2
- package/templates/template/frontend/vite.config.ts +8 -0
- package/templates/template/gitignore +19 -1
- package/templates/template/npmrc +13 -4
- package/templates/template/package.json +10 -1
- package/templates/template/pnpm-workspace.yaml +17 -0
- package/templates/template/scripts/example.ts +4 -1
- package/dist/daemon-Bdl4lrdt.js.map +0 -1
- package/dist/daemon-entry-CmJn83zu.js.map +0 -1
- package/dist/pull-DqPRu1te.js.map +0 -1
- package/dist/state-c0CJ6Kwb.js.map +0 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
+
//#region src/dev-db/resolve.ts
|
|
3
|
+
var resolve_exports = /* @__PURE__ */ __exportAll({
|
|
4
|
+
describeDevDatabase: () => describeDevDatabase,
|
|
5
|
+
resolveDevDatabase: () => resolveDevDatabase
|
|
6
|
+
});
|
|
7
|
+
/**
|
|
8
|
+
* A value that is present but empty is treated as absent.
|
|
9
|
+
*
|
|
10
|
+
* `DATABASE_URL=` in a `.env` is what a developer writes when they mean "not
|
|
11
|
+
* this one" — honouring it literally would hand an empty connection string to
|
|
12
|
+
* libpq and produce an error about a missing host, which explains nothing.
|
|
13
|
+
*/
|
|
14
|
+
function present(value) {
|
|
15
|
+
if (typeof value !== "string") return null;
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
18
|
+
}
|
|
19
|
+
function resolveDevDatabase(input = {}) {
|
|
20
|
+
const flagUrl = present(input.flagUrl);
|
|
21
|
+
if (flagUrl) return {
|
|
22
|
+
kind: "external",
|
|
23
|
+
url: flagUrl,
|
|
24
|
+
source: "flag"
|
|
25
|
+
};
|
|
26
|
+
const fromEnvironment = present(input.env?.DATABASE_URL);
|
|
27
|
+
if (fromEnvironment) return {
|
|
28
|
+
kind: "external",
|
|
29
|
+
url: fromEnvironment,
|
|
30
|
+
source: "environment"
|
|
31
|
+
};
|
|
32
|
+
const fromBranch = present(input.branch?.url);
|
|
33
|
+
if (fromBranch && input.branch) return {
|
|
34
|
+
kind: "external",
|
|
35
|
+
url: fromBranch,
|
|
36
|
+
source: "branch",
|
|
37
|
+
branch: input.branch.name
|
|
38
|
+
};
|
|
39
|
+
const fromEnvFile = present(input.envFile?.DATABASE_URL);
|
|
40
|
+
if (fromEnvFile) return {
|
|
41
|
+
kind: "external",
|
|
42
|
+
url: fromEnvFile,
|
|
43
|
+
source: "env-file"
|
|
44
|
+
};
|
|
45
|
+
if (input.flagDocker || input.manifestPreference === "docker") return {
|
|
46
|
+
kind: "docker",
|
|
47
|
+
source: "docker",
|
|
48
|
+
url: present(input.composeUrl)
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
kind: "managed",
|
|
52
|
+
source: "managed"
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** One line for the startup banner, naming both the database and why. */
|
|
56
|
+
function describeDevDatabase(database) {
|
|
57
|
+
switch (database.kind) {
|
|
58
|
+
case "external":
|
|
59
|
+
switch (database.source) {
|
|
60
|
+
case "flag": return "your database (--database-url)";
|
|
61
|
+
case "environment": return "your database (DATABASE_URL in the environment)";
|
|
62
|
+
case "env-file": return "your database (DATABASE_URL in .env)";
|
|
63
|
+
case "branch": return `branch "${database.branch}" (rebase db branch switch)`;
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
case "docker": return "Postgres in Docker";
|
|
67
|
+
case "managed": return "the managed development database (PGlite)";
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`Unhandled dev database: ${JSON.stringify(database)}`);
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { resolveDevDatabase as n, resolve_exports as r, describeDevDatabase as t };
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=resolve-Y56osuQH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-Y56osuQH.js","names":[],"sources":["../src/dev-db/resolve.ts"],"sourcesContent":["/**\n * Which database a command should talk to, decided in one place.\n *\n * Before this existed every command that needed Postgres read `DATABASE_URL`\n * for itself, which was fine while there was exactly one answer. Introducing a\n * managed database makes the question real: a project may have no\n * `DATABASE_URL` at all and still expect `rebase db push` to work, and a\n * project that *does* set one must never be quietly redirected somewhere else.\n *\n * So the rule is ordered and boring, and the order is the promise:\n *\n * 1. `--database-url <url>` — said on this command line, wins over everything\n * 2. `DATABASE_URL` in the shell environment\n * 3. the database branch this checkout is switched to\n * 4. `DATABASE_URL` in the project's `.env`\n * 5. `--docker` / a manifest preference of `docker`\n * 6. the managed PGlite database\n *\n * An explicit connection string always wins. That is the whole point of the\n * override: someone pointing Rebase at their own Postgres — a colleague's\n * staging box, a Neon branch, a container they manage — must get exactly that,\n * with no cleverness in between. The managed database is what fills the vacuum\n * when nobody has said anything, and it is the only case where the CLI picks.\n *\n * The branch at (3) is the one addition that is not an explicit connection\n * string, and it sits where it does deliberately. It has to outrank `.env` or\n * `rebase db branch switch` would silently do nothing on every project that\n * sets `DATABASE_URL` — which is every project not on the managed database.\n * It must not outrank (1) or (2), because a flag on this command line and a\n * variable in this shell are more immediate than a switch made yesterday.\n *\n * {@link resolveDevDatabase} is pure: inputs in, decision out, no filesystem\n * and no process. Reading `.env`, reading the branch pointer and starting a\n * daemon happen elsewhere, so the ordering above can be tested without any of\n * them.\n */\n\n/** Where the answer came from. Carried so diagnostics can name it. */\nexport type DevDatabaseSource =\n /** `--database-url` on the command line. */\n | \"flag\"\n /** `DATABASE_URL` in the shell environment. */\n | \"environment\"\n /** `DATABASE_URL` in the project's `.env`. */\n | \"env-file\"\n /** The branch this checkout is switched to, over the base connection. */\n | \"branch\"\n /** `--docker`, or `devDatabase: \"docker\"` in the manifest. */\n | \"docker\"\n /** Nobody said anything, so the managed database fills in. */\n | \"managed\";\n\nexport type DevDatabase =\n | {\n kind: \"external\";\n /** The connection string, exactly as given. Never rewritten. */\n url: string;\n source: Extract<DevDatabaseSource, \"flag\" | \"environment\" | \"env-file\" | \"branch\">;\n /** The branch name, when this checkout is switched to one. */\n branch?: string;\n }\n | {\n kind: \"docker\";\n source: \"docker\";\n /**\n * The compose `db` service's connection string, derived by the caller.\n *\n * Null when the project has no compose file to derive one from, which\n * is a `--docker` that cannot be honoured rather than a fallback: the\n * caller says so instead of guessing at `localhost:5432`.\n */\n url: string | null;\n }\n | {\n kind: \"managed\";\n source: \"managed\";\n };\n\nexport interface ResolveDevDatabaseInput {\n /** `--database-url <url>`, if given. */\n flagUrl?: string | null;\n /** `--docker`, if given. */\n flagDocker?: boolean;\n /** The shell environment. Only `DATABASE_URL` is read. */\n env?: Record<string, string | undefined>;\n /** Parsed `.env` from the project root. Only `DATABASE_URL` is read. */\n envFile?: Record<string, string> | null;\n /** `devDatabase` from `rebase.json`, if the project recorded a preference. */\n manifestPreference?: \"managed\" | \"docker\" | null;\n /**\n * The compose `db` service's connection string, when the project has one.\n *\n * Passed in rather than read here so this function stays pure. Only\n * consulted for the Docker case; an explicit `DATABASE_URL` still wins.\n */\n composeUrl?: string | null;\n /**\n * The branch this checkout is switched to, already resolved to a URL.\n *\n * Resolved by the caller rather than here so this stays pure: deriving it\n * needs the base connection string, which needs the `.env` this function is\n * handed rather than reads.\n */\n branch?: { name: string; url: string } | null;\n}\n\n/**\n * A value that is present but empty is treated as absent.\n *\n * `DATABASE_URL=` in a `.env` is what a developer writes when they mean \"not\n * this one\" — honouring it literally would hand an empty connection string to\n * libpq and produce an error about a missing host, which explains nothing.\n */\nfunction present(value: string | undefined | null): string | null {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n\n return trimmed.length > 0 ? trimmed : null;\n}\n\nexport function resolveDevDatabase(input: ResolveDevDatabaseInput = {}): DevDatabase {\n const flagUrl = present(input.flagUrl);\n if (flagUrl) return { kind: \"external\", url: flagUrl, source: \"flag\" };\n\n const fromEnvironment = present(input.env?.DATABASE_URL);\n if (fromEnvironment) return { kind: \"external\", url: fromEnvironment, source: \"environment\" };\n\n // Above `.env`, below anything said explicitly — see the ordering note at\n // the top of this file.\n const fromBranch = present(input.branch?.url);\n if (fromBranch && input.branch) {\n return { kind: \"external\", url: fromBranch, source: \"branch\", branch: input.branch.name };\n }\n\n const fromEnvFile = present(input.envFile?.DATABASE_URL);\n if (fromEnvFile) return { kind: \"external\", url: fromEnvFile, source: \"env-file\" };\n\n // Only consulted once every explicit connection string is exhausted: asking\n // for Docker is a choice about *how to get* a database, not which one, so a\n // DATABASE_URL that already names one outranks it.\n if (input.flagDocker || input.manifestPreference === \"docker\") {\n return { kind: \"docker\", source: \"docker\", url: present(input.composeUrl) };\n }\n\n return { kind: \"managed\", source: \"managed\" };\n}\n\n/** One line for the startup banner, naming both the database and why. */\nexport function describeDevDatabase(database: DevDatabase): string {\n switch (database.kind) {\n case \"external\":\n switch (database.source) {\n case \"flag\":\n return \"your database (--database-url)\";\n case \"environment\":\n return \"your database (DATABASE_URL in the environment)\";\n case \"env-file\":\n return \"your database (DATABASE_URL in .env)\";\n case \"branch\":\n return `branch \"${database.branch}\" (rebase db branch switch)`;\n }\n break;\n case \"docker\":\n return \"Postgres in Docker\";\n case \"managed\":\n return \"the managed development database (PGlite)\";\n }\n\n // Unreachable while the union is exhaustive; kept so a future variant fails\n // loudly in review rather than printing \"undefined\" to a user.\n throw new Error(`Unhandled dev database: ${JSON.stringify(database)}`);\n}\n"],"mappings":";;;;;;;;;;;;;AAiHA,SAAS,QAAQ,OAAiD;CAC9D,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,MAAM,UAAU,MAAM,KAAK;CAE3B,OAAO,QAAQ,SAAS,IAAI,UAAU;AAC1C;AAEA,SAAgB,mBAAmB,QAAiC,CAAC,GAAgB;CACjF,MAAM,UAAU,QAAQ,MAAM,OAAO;CACrC,IAAI,SAAS,OAAO;EAAE,MAAM;EAAY,KAAK;EAAS,QAAQ;CAAO;CAErE,MAAM,kBAAkB,QAAQ,MAAM,KAAK,YAAY;CACvD,IAAI,iBAAiB,OAAO;EAAE,MAAM;EAAY,KAAK;EAAiB,QAAQ;CAAc;CAI5F,MAAM,aAAa,QAAQ,MAAM,QAAQ,GAAG;CAC5C,IAAI,cAAc,MAAM,QACpB,OAAO;EAAE,MAAM;EAAY,KAAK;EAAY,QAAQ;EAAU,QAAQ,MAAM,OAAO;CAAK;CAG5F,MAAM,cAAc,QAAQ,MAAM,SAAS,YAAY;CACvD,IAAI,aAAa,OAAO;EAAE,MAAM;EAAY,KAAK;EAAa,QAAQ;CAAW;CAKjF,IAAI,MAAM,cAAc,MAAM,uBAAuB,UACjD,OAAO;EAAE,MAAM;EAAU,QAAQ;EAAU,KAAK,QAAQ,MAAM,UAAU;CAAE;CAG9E,OAAO;EAAE,MAAM;EAAW,QAAQ;CAAU;AAChD;;AAGA,SAAgB,oBAAoB,UAA+B;CAC/D,QAAQ,SAAS,MAAjB;EACI,KAAK;GACD,QAAQ,SAAS,QAAjB;IACI,KAAK,QACD,OAAO;IACX,KAAK,eACD,OAAO;IACX,KAAK,YACD,OAAO;IACX,KAAK,UACD,OAAO,WAAW,SAAS,OAAO;GAC1C;GACA;EACJ,KAAK,UACD,OAAO;EACX,KAAK,WACD,OAAO;CACf;CAIA,MAAM,IAAI,MAAM,2BAA2B,KAAK,UAAU,QAAQ,GAAG;AACzE"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ResourceGraph } from "@rebasepro/types";
|
|
1
|
+
import { type RebaseBackendAppConfig, type ResourceDeclaration, type ResourceGraph } from "@rebasepro/types";
|
|
2
2
|
/** The committed, generated record of what a project needs. */
|
|
3
3
|
export declare const RESOURCE_GRAPH_FILENAME = "rebase.resources.json";
|
|
4
4
|
/** A problem found while deriving, reported with the rest rather than thrown one at a time. */
|
|
@@ -6,6 +6,20 @@ export interface ResourceIssue {
|
|
|
6
6
|
path: string;
|
|
7
7
|
message: string;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* The message an issue carries, with the skew named when there is one.
|
|
11
|
+
*
|
|
12
|
+
* `main` runs 497 commits ahead of npm `latest` and the docs site publishes from
|
|
13
|
+
* `main`, so the shape this exists for is routine: a scaffold pinned to the
|
|
14
|
+
* published version, config code written against today's documentation, and an
|
|
15
|
+
* error about the config file. The fix is one command and the error named none
|
|
16
|
+
* of it — the same failure as `404 No PUT route on collection 'projects'`, one
|
|
17
|
+
* layer down.
|
|
18
|
+
*
|
|
19
|
+
* Says nothing extra when the two versions agree, or when either is unreadable:
|
|
20
|
+
* a sentence naming one version twice is worse than no sentence.
|
|
21
|
+
*/
|
|
22
|
+
export declare function describeIssue(err: unknown, projectRoot: string, ownVersion?: string): string;
|
|
9
23
|
export interface DeriveOptions {
|
|
10
24
|
/** Absolute path to the project's config directory. */
|
|
11
25
|
configDir: string;
|
|
@@ -16,7 +30,23 @@ export interface DeriveOptions {
|
|
|
16
30
|
* would under-report what the project needs.
|
|
17
31
|
*/
|
|
18
32
|
includeCollections?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Absolute path to the crons directory. Each file is evaluated through the
|
|
35
|
+
* same loader the runtime uses, which declares the cron under the same id
|
|
36
|
+
* the scheduler runs it as. Absent, or missing on disk: no crons.
|
|
37
|
+
*/
|
|
38
|
+
cronsDir?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Absolute path to the functions directory. Read statically — the
|
|
41
|
+
* bundler's portability analysis — never evaluated: a function's module
|
|
42
|
+
* scope belongs to a running backend, not a build.
|
|
43
|
+
*/
|
|
44
|
+
functionsDir?: string;
|
|
45
|
+
/** The project root, for the paths a graph records. Defaults to the config directory's parent. */
|
|
46
|
+
projectRoot?: string;
|
|
19
47
|
}
|
|
48
|
+
/** The derive options for a backend app, from its manifest entry. */
|
|
49
|
+
export declare function deriveOptionsFor(projectRoot: string, app: RebaseBackendAppConfig): DeriveOptions;
|
|
20
50
|
/**
|
|
21
51
|
* Evaluate a project's config and return the graph it declares.
|
|
22
52
|
*
|
|
@@ -45,3 +75,39 @@ export declare function writeResourceGraphFile(projectRoot: string, graph: Resou
|
|
|
45
75
|
};
|
|
46
76
|
/** Parse a committed graph file, tolerating the `$generated` banner. */
|
|
47
77
|
export declare function parseResourceGraph(contents: string): ResourceGraph;
|
|
78
|
+
/**
|
|
79
|
+
* Add the resources a project has without declaring them.
|
|
80
|
+
*
|
|
81
|
+
* A backend has a database whether or not anyone said so, and a project that
|
|
82
|
+
* declares no buckets still gets one default storage source from the plain
|
|
83
|
+
* unsuffixed variables. Both are load-bearing defaults and both are invisible
|
|
84
|
+
* in the graph, so a status view built only from declarations would show an
|
|
85
|
+
* empty screen to the majority of projects — the ones that most need to be told
|
|
86
|
+
* which variable their one database reads.
|
|
87
|
+
*/
|
|
88
|
+
export declare function withImplicitDefaults(graph: ResourceGraph): {
|
|
89
|
+
declaration: ResourceDeclaration;
|
|
90
|
+
implicit: boolean;
|
|
91
|
+
}[];
|
|
92
|
+
/**
|
|
93
|
+
* The resources a project *has*, which is what a person is asking about.
|
|
94
|
+
*
|
|
95
|
+
* One projection, because two of them disagreed. `rebase status` built its
|
|
96
|
+
* rows from {@link withImplicitDefaults} and listed `buckets ✓ (default) local
|
|
97
|
+
* · implicit`; `rebase resources` and `rebase resources --json` built theirs
|
|
98
|
+
* from the raw graph and listed only the database and the function. Two
|
|
99
|
+
* commands whose whole job is to answer "what does this project need", on the
|
|
100
|
+
* same stock scaffold, giving different answers.
|
|
101
|
+
*
|
|
102
|
+
* The implicit entries are marked rather than hidden, because the distinction
|
|
103
|
+
* is real and load-bearing: a declared resource is recorded in
|
|
104
|
+
* `rebase.resources.json` for a host to provision, and an implicit one is a
|
|
105
|
+
* default the runtime supplies whether or not anyone wrote it down. That file
|
|
106
|
+
* still holds declarations only — it is a wire contract a host reads, and
|
|
107
|
+
* putting defaults in it would ask for something to be provisioned that
|
|
108
|
+
* nobody declared.
|
|
109
|
+
*/
|
|
110
|
+
export type ProjectedResource = ResourceDeclaration & {
|
|
111
|
+
implicit?: true;
|
|
112
|
+
};
|
|
113
|
+
export declare function projectResourceGraph(graph: ResourceGraph): ProjectedResource[];
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { type ResourceDeclaration, type ResourceGraph } from "@rebasepro/types";
|
|
2
|
+
/** One environment variable a resource reads, and whether it is there. */
|
|
3
|
+
export interface ResourceBinding {
|
|
4
|
+
/** The variable this resource reads, suffix included. */
|
|
5
|
+
name: string;
|
|
6
|
+
set: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* The account-scoped name consulted when `name` is unset.
|
|
9
|
+
*
|
|
10
|
+
* Only provider-level bindings have one, and only when the resource named
|
|
11
|
+
* an `account`. Shown because a developer looking at an unset
|
|
12
|
+
* `S3_ACCESS_KEY_ID__MEDIA` needs to know it is not the whole story.
|
|
13
|
+
*/
|
|
14
|
+
fallback?: {
|
|
15
|
+
name: string;
|
|
16
|
+
set: boolean;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Whether a declared resource can actually be reached. */
|
|
20
|
+
export type ResourceState =
|
|
21
|
+
/** Bound and usable. */
|
|
22
|
+
"ready"
|
|
23
|
+
/** Nothing set for it. Legal for a bucket, fatal for a database. */
|
|
24
|
+
| "unconfigured"
|
|
25
|
+
/** Set, and set wrongly. The deployment refuses to start. */
|
|
26
|
+
| "broken";
|
|
27
|
+
export interface ResourceStatus {
|
|
28
|
+
kind: string;
|
|
29
|
+
key: string;
|
|
30
|
+
engine: string;
|
|
31
|
+
transport: string;
|
|
32
|
+
/** Present when this resource shares another's credentials. */
|
|
33
|
+
account?: string;
|
|
34
|
+
/** True for a resource nobody declared, which exists anyway. */
|
|
35
|
+
implicit: boolean;
|
|
36
|
+
bindings: ResourceBinding[];
|
|
37
|
+
state: ResourceState;
|
|
38
|
+
/** What this means, in the terms the developer will meet it in. */
|
|
39
|
+
detail: string;
|
|
40
|
+
/** Set when a local directory stands in for this engine, in development. */
|
|
41
|
+
standsIn?: string;
|
|
42
|
+
/** What in the project reaches this resource, from the graph. */
|
|
43
|
+
usedBy?: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
export type EnvBag = Record<string, string | undefined>;
|
|
46
|
+
/**
|
|
47
|
+
* The bindings one declaration reads, in the order a reader should scan them.
|
|
48
|
+
*
|
|
49
|
+
* A `direct`-transport resource gets none: the browser reaches it with a
|
|
50
|
+
* provider SDK and the backend binds nothing for it, so listing variables would
|
|
51
|
+
* invite someone to set variables that are never read.
|
|
52
|
+
*/
|
|
53
|
+
export declare function bindingsFor(declaration: ResourceDeclaration, env: EnvBag, accountScopedBases: readonly string[]): ResourceBinding[];
|
|
54
|
+
/**
|
|
55
|
+
* What the managed development database covers, when a project is on it.
|
|
56
|
+
*
|
|
57
|
+
* `rebase init` leaves `DATABASE_URL` commented out on purpose and the managed
|
|
58
|
+
* PGlite fills the vacuum — the documented first-run state. Judging that state
|
|
59
|
+
* by the environment alone produced `○ (default) postgres · DATABASE_URL not
|
|
60
|
+
* set` and the remedy "set DATABASE_URL", at the same moment `rebase db url`
|
|
61
|
+
* was printing a working connection string for the same project and
|
|
62
|
+
* `rebase dev` was serving from it. The status view was reading the one place
|
|
63
|
+
* the answer deliberately is not.
|
|
64
|
+
*
|
|
65
|
+
* Resolved by the caller, because deciding it needs the project's `.env`, its
|
|
66
|
+
* branch pointer and its compose file, and this module stays free of all three.
|
|
67
|
+
*/
|
|
68
|
+
export interface ManagedDatabaseStatus {
|
|
69
|
+
/**
|
|
70
|
+
* The daemon's connection string, or null when it is not running.
|
|
71
|
+
*
|
|
72
|
+
* Null is not an error: the managed database exists per project and starts
|
|
73
|
+
* on demand. It only means there is nothing to run the whole-set check
|
|
74
|
+
* against, and the row says so rather than implying a misconfiguration.
|
|
75
|
+
*/
|
|
76
|
+
url: string | null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The resolver-side of status, as `@rebasepro/server` registers it per kind.
|
|
80
|
+
*
|
|
81
|
+
* Passed in rather than imported so this module stays testable without a
|
|
82
|
+
* server, and so a kind registered by a plugin is judged by the resolver the
|
|
83
|
+
* plugin brought with it rather than by a switch here.
|
|
84
|
+
*/
|
|
85
|
+
export interface StatusResolvers {
|
|
86
|
+
/** `resourceResolver(kind)` from `@rebasepro/server`. */
|
|
87
|
+
resolverFor: (kind: string) => {
|
|
88
|
+
accountScoped?: readonly string[];
|
|
89
|
+
resolve: (declaration: ResourceDeclaration, env: EnvBag, context: {
|
|
90
|
+
production: boolean;
|
|
91
|
+
defaultBasePath: string;
|
|
92
|
+
}) => {
|
|
93
|
+
state: "ready" | "unbound" | "blocked" | "code";
|
|
94
|
+
detail?: string;
|
|
95
|
+
standsIn?: string;
|
|
96
|
+
};
|
|
97
|
+
} | undefined;
|
|
98
|
+
/** `resolveDataSources` from `@rebasepro/server`, for the whole-set check. */
|
|
99
|
+
resolveDataSources: (env: EnvBag, definitions: unknown) => unknown;
|
|
100
|
+
/** Whether the process being judged is production. Status judges for development by default. */
|
|
101
|
+
production?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Set when this project runs on the managed development database.
|
|
104
|
+
*
|
|
105
|
+
* Absent for a project that named its own connection string, and absent in
|
|
106
|
+
* production — where an unset `DATABASE_URL` is the failure it has always
|
|
107
|
+
* been.
|
|
108
|
+
*/
|
|
109
|
+
managedDatabase?: ManagedDatabaseStatus;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Resolve every declared resource against an environment.
|
|
113
|
+
*
|
|
114
|
+
* Each kind's verdict comes from the resolver `@rebasepro/server` registered
|
|
115
|
+
* for it — the same one boot calls — so this view cannot say "ready" about a
|
|
116
|
+
* binding boot would refuse. A kind with no resolver is reported as such
|
|
117
|
+
* rather than skipped: a resource nobody shows is one nobody remembers to
|
|
118
|
+
* configure, and a kind this runtime cannot bind is exactly what boot will
|
|
119
|
+
* refuse by name.
|
|
120
|
+
*
|
|
121
|
+
* `resolveDataSources` is then run once over the whole set, so anything it
|
|
122
|
+
* would refuse across sources (a driver package it cannot resolve for a custom
|
|
123
|
+
* engine) is reported rather than missed by a view that only looked per row.
|
|
124
|
+
*/
|
|
125
|
+
export declare function computeStatus(graph: ResourceGraph, env: EnvBag, resolvers: StatusResolvers): {
|
|
126
|
+
resources: ResourceStatus[];
|
|
127
|
+
blocked?: string;
|
|
128
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll as t };
|
|
@@ -1,18 +1,8 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
1
2
|
import path from "path";
|
|
2
3
|
import fs from "fs";
|
|
3
4
|
import net from "net";
|
|
4
|
-
|
|
5
|
-
var __defProp = Object.defineProperty;
|
|
6
|
-
var __exportAll = (all, no_symbols) => {
|
|
7
|
-
let target = {};
|
|
8
|
-
for (var name in all) __defProp(target, name, {
|
|
9
|
-
get: all[name],
|
|
10
|
-
enumerable: true
|
|
11
|
-
});
|
|
12
|
-
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
-
return target;
|
|
14
|
-
};
|
|
15
|
-
//#endregion
|
|
5
|
+
import { resourceEnvSuffix } from "@rebasepro/types";
|
|
16
6
|
//#region src/dev-db/state.ts
|
|
17
7
|
/**
|
|
18
8
|
* The managed database's state file, and the rules for trusting it.
|
|
@@ -34,6 +24,7 @@ var state_exports = /* @__PURE__ */ __exportAll({
|
|
|
34
24
|
START_LOCK_NAME: () => START_LOCK_NAME,
|
|
35
25
|
STATE_FILE_NAME: () => STATE_FILE_NAME,
|
|
36
26
|
acquireStartLock: () => acquireStartLock,
|
|
27
|
+
additionalDataDir: () => additionalDataDir,
|
|
37
28
|
clearState: () => clearState,
|
|
38
29
|
dataDir: () => dataDir,
|
|
39
30
|
devDbDir: () => devDbDir,
|
|
@@ -67,6 +58,16 @@ function devDbDir(projectRoot) {
|
|
|
67
58
|
function dataDir(projectRoot) {
|
|
68
59
|
return path.join(devDbDir(projectRoot), DATA_DIR_NAME);
|
|
69
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* The data directory for an additional declared database.
|
|
63
|
+
*
|
|
64
|
+
* `pgdata__analytics`: the key spelled the way its variable is, so the
|
|
65
|
+
* directory beside `pgdata` and `DATABASE_URL__ANALYTICS` are visibly the
|
|
66
|
+
* same thing.
|
|
67
|
+
*/
|
|
68
|
+
function additionalDataDir(projectRoot, key) {
|
|
69
|
+
return path.join(devDbDir(projectRoot), `${DATA_DIR_NAME}${resourceEnvSuffix(key).toLowerCase()}`);
|
|
70
|
+
}
|
|
70
71
|
function stateFile(projectRoot) {
|
|
71
72
|
return path.join(devDbDir(projectRoot), STATE_FILE_NAME);
|
|
72
73
|
}
|
|
@@ -128,13 +129,21 @@ function readState(projectRoot) {
|
|
|
128
129
|
try {
|
|
129
130
|
const parsed = JSON.parse(raw);
|
|
130
131
|
if (typeof parsed.port !== "number" || !Number.isInteger(parsed.port) || parsed.port <= 0 || parsed.port > 65535 || typeof parsed.pid !== "number" || typeof parsed.dataDir !== "string" || typeof parsed.token !== "string" || parsed.token.length === 0 || typeof parsed.identityPort !== "number" || !Number.isInteger(parsed.identityPort) || parsed.identityPort <= 0 || parsed.identityPort > 65535) return null;
|
|
132
|
+
const databases = {};
|
|
133
|
+
if (parsed.databases && typeof parsed.databases === "object") {
|
|
134
|
+
for (const [key, entry] of Object.entries(parsed.databases)) if (entry && typeof entry.port === "number" && Number.isInteger(entry.port) && entry.port > 0 && entry.port <= 65535 && typeof entry.dataDir === "string") databases[key] = {
|
|
135
|
+
port: entry.port,
|
|
136
|
+
dataDir: entry.dataDir
|
|
137
|
+
};
|
|
138
|
+
}
|
|
131
139
|
return {
|
|
132
140
|
port: parsed.port,
|
|
133
141
|
pid: parsed.pid,
|
|
134
142
|
dataDir: parsed.dataDir,
|
|
135
143
|
startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : "",
|
|
136
144
|
token: parsed.token,
|
|
137
|
-
identityPort: parsed.identityPort
|
|
145
|
+
identityPort: parsed.identityPort,
|
|
146
|
+
...Object.keys(databases).length > 0 ? { databases } : {}
|
|
138
147
|
};
|
|
139
148
|
} catch {
|
|
140
149
|
return null;
|
|
@@ -185,6 +194,6 @@ function findFreePort() {
|
|
|
185
194
|
});
|
|
186
195
|
}
|
|
187
196
|
//#endregion
|
|
188
|
-
export {
|
|
197
|
+
export { devDbDir as a, readState as c, state_exports as d, writeState as f, dataDir as i, releaseStartLock as l, additionalDataDir as n, findFreePort as o, clearState as r, pidRunning as s, acquireStartLock as t, stateFile as u };
|
|
189
198
|
|
|
190
|
-
//# sourceMappingURL=state-
|
|
199
|
+
//# sourceMappingURL=state-C59Elrnt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-C59Elrnt.js","names":[],"sources":["../src/dev-db/state.ts"],"sourcesContent":["/**\n * The managed database's state file, and the rules for trusting it.\n *\n * The daemon outlives the command that started it — `rebase db push` in one\n * terminal and `rebase dev` in another have to reach the *same* PGlite, because\n * two processes opening one data directory would corrupt it. So the daemon\n * records where it is, and every command reads that record.\n *\n * A record on disk is a claim, not a fact. The process it names may have been\n * killed, the machine may have rebooted and handed the pid to something else,\n * and the port may now belong to a stranger. {@link readState} therefore only\n * parses; deciding whether a record is live is {@link isDaemonAlive}'s job, and\n * it asks the daemon rather than the operating system.\n */\n\nimport fs from \"fs\";\nimport { resourceEnvSuffix } from \"@rebasepro/types\";\nimport net from \"net\";\nimport path from \"path\";\n\n/** Everything under here is generated and gitignored. */\nexport const DEV_DB_DIR = \".rebase\";\n/** PGlite's own data directory. Deleting it is what `--reset` means. */\nexport const DATA_DIR_NAME = \"pgdata\";\n/** The record the daemon writes once it is accepting connections. */\nexport const STATE_FILE_NAME = \"pglite.json\";\n/**\n * Held by whoever is currently starting a daemon.\n *\n * Without it, `rebase dev` and `rebase db push` started in the same second both\n * see no state file, both spawn, and two processes open one PGlite data\n * directory — the exact corruption the single-daemon design exists to prevent.\n * Observed as `ENOTEMPTY` during cleanup, which is the harmless way for it to\n * show up; the harmful way is a damaged database.\n */\nexport const START_LOCK_NAME = \"starting.lock\";\n\nexport interface DaemonState {\n /** TCP port the socket server is listening on, chosen when it started. */\n port: number;\n /** The daemon process. Used only as a fast negative check. */\n pid: number;\n /** Absolute path of the PGlite data directory this daemon has open. */\n dataDir: string;\n /** ISO timestamp, for diagnostics. */\n startedAt: string;\n /**\n * A random token the daemon also answers with over the wire, on\n * {@link identityPort}.\n *\n * Without it, \"is the daemon alive?\" degrades to \"is something listening on\n * that port?\", which is a different question and answers yes for whatever\n * process happened to take the port after a reboot. Rebase would then send\n * migrations to a stranger.\n */\n token: string;\n /** Loopback port that answers the identity check. */\n identityPort: number;\n /**\n * The additional databases this daemon serves, by declared key.\n *\n * One PGlite instance each — PGlite is one database per instance, so\n * `database(\"analytics\")` is a second data directory and a second port,\n * not a `CREATE DATABASE`. Started on demand when a command asks for the\n * key over the identity socket, and recorded here so `rebase status` and\n * a reset can see them. The default database is the fields above.\n */\n databases?: Record<string, { port: number; dataDir: string }>;\n}\n\nexport function devDbDir(projectRoot: string): string {\n return path.join(projectRoot, DEV_DB_DIR);\n}\n\nexport function dataDir(projectRoot: string): string {\n return path.join(devDbDir(projectRoot), DATA_DIR_NAME);\n}\n\n/**\n * The data directory for an additional declared database.\n *\n * `pgdata__analytics`: the key spelled the way its variable is, so the\n * directory beside `pgdata` and `DATABASE_URL__ANALYTICS` are visibly the\n * same thing.\n */\nexport function additionalDataDir(projectRoot: string, key: string): string {\n return path.join(devDbDir(projectRoot), `${DATA_DIR_NAME}${resourceEnvSuffix(key).toLowerCase()}`);\n}\n\nexport function stateFile(projectRoot: string): string {\n return path.join(devDbDir(projectRoot), STATE_FILE_NAME);\n}\n\nexport function startLockFile(projectRoot: string): string {\n return path.join(devDbDir(projectRoot), START_LOCK_NAME);\n}\n\n/**\n * Take the start lock, or report that somebody else holds it.\n *\n * `wx` is the whole mechanism: create-if-absent is a single atomic syscall, so\n * exactly one of two racing processes can succeed no matter how close together\n * they arrive.\n *\n * A lock older than `staleAfterMs` is broken rather than waited on — the holder\n * may have been killed between creating it and starting anything, and a\n * developer should never have to know this file exists in order to unstick\n * their project.\n */\nexport function acquireStartLock(projectRoot: string, staleAfterMs: number): boolean {\n const target = startLockFile(projectRoot);\n fs.mkdirSync(devDbDir(projectRoot), { recursive: true });\n\n const attempt = (): boolean => {\n try {\n const handle = fs.openSync(target, \"wx\");\n fs.writeSync(handle, `${process.pid} ${new Date().toISOString()}\\n`);\n fs.closeSync(handle);\n\n return true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n\n return false;\n }\n };\n\n if (attempt()) return true;\n\n try {\n // Clamped at zero: a filesystem whose timestamp granularity rounds the\n // mtime *up* reports a negative age for a lock created moments ago,\n // and a negative age is below every threshold — so `staleAfterMs: 0`,\n // which means \"break any lock\", would refuse to break one.\n const age = Math.max(0, Date.now() - fs.statSync(target).mtimeMs);\n if (age < staleAfterMs) return false;\n fs.unlinkSync(target);\n } catch {\n // Vanished under us, which means the holder finished. Either way the\n // next attempt is the answer.\n }\n\n return attempt();\n}\n\nexport function releaseStartLock(projectRoot: string): void {\n try {\n fs.unlinkSync(startLockFile(projectRoot));\n } catch {\n // Already released is the desired end state.\n }\n}\n\n/**\n * Parse the record, or `null` for anything that is not one.\n *\n * Every failure is the same answer — absent — because every failure has the\n * same remedy: start a daemon. A corrupt state file is not worth an error\n * message to a user who never wrote it.\n */\nexport function readState(projectRoot: string): DaemonState | null {\n let raw: string;\n try {\n raw = fs.readFileSync(stateFile(projectRoot), \"utf8\");\n } catch {\n return null;\n }\n\n try {\n const parsed = JSON.parse(raw) as Partial<DaemonState>;\n if (\n typeof parsed.port !== \"number\" ||\n !Number.isInteger(parsed.port) ||\n parsed.port <= 0 ||\n parsed.port > 65535 ||\n typeof parsed.pid !== \"number\" ||\n typeof parsed.dataDir !== \"string\" ||\n typeof parsed.token !== \"string\" ||\n parsed.token.length === 0 ||\n typeof parsed.identityPort !== \"number\" ||\n !Number.isInteger(parsed.identityPort) ||\n parsed.identityPort <= 0 ||\n parsed.identityPort > 65535\n ) {\n return null;\n }\n\n // Only well-formed entries survive: a malformed one is treated as\n // absent, and the next ensure asks the daemon for it again.\n const databases: Record<string, { port: number; dataDir: string }> = {};\n if (parsed.databases && typeof parsed.databases === \"object\") {\n for (const [key, entry] of Object.entries(parsed.databases)) {\n if (\n entry && typeof entry.port === \"number\" && Number.isInteger(entry.port)\n && entry.port > 0 && entry.port <= 65535 && typeof entry.dataDir === \"string\"\n ) {\n databases[key] = { port: entry.port, dataDir: entry.dataDir };\n }\n }\n }\n\n return {\n port: parsed.port,\n pid: parsed.pid,\n dataDir: parsed.dataDir,\n startedAt: typeof parsed.startedAt === \"string\" ? parsed.startedAt : \"\",\n token: parsed.token,\n identityPort: parsed.identityPort,\n ...(Object.keys(databases).length > 0 ? { databases } : {})\n };\n } catch {\n return null;\n }\n}\n\nexport function writeState(projectRoot: string, state: DaemonState): void {\n fs.mkdirSync(devDbDir(projectRoot), { recursive: true });\n // Written whole then moved, so a reader never sees half a record — commands\n // poll this file while the daemon is starting.\n const target = stateFile(projectRoot);\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\\n`, \"utf8\");\n fs.renameSync(temporary, target);\n}\n\nexport function clearState(projectRoot: string): void {\n try {\n fs.unlinkSync(stateFile(projectRoot));\n } catch {\n // Already gone is the desired end state.\n }\n}\n\n/** Is *some* process with this pid running? A fast, cheap negative check. */\nexport function pidRunning(pid: number): boolean {\n try {\n // Signal 0 performs the permission and existence checks without\n // delivering anything.\n process.kill(pid, 0);\n\n return true;\n } catch (error) {\n // EPERM means it exists and belongs to someone else, which for our\n // purposes is still \"running\".\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/** Can a TCP connection be opened to this port on loopback? */\nexport function portAccepting(port: number, timeoutMs = 1000): Promise<boolean> {\n return new Promise((resolve) => {\n const socket = new net.Socket();\n const settle = (answer: boolean) => {\n socket.removeAllListeners();\n socket.destroy();\n resolve(answer);\n };\n socket.setTimeout(timeoutMs);\n socket.once(\"connect\", () => settle(true));\n socket.once(\"timeout\", () => settle(false));\n socket.once(\"error\", () => settle(false));\n socket.connect(port, \"127.0.0.1\");\n });\n}\n\n/**\n * Ask a port for a free one, then hand back the number.\n *\n * Deliberately not the probe `rebase init` uses: that one has a documented\n * failure where a port is free to probe and unusable to publish. This binds on\n * loopback only, which is also where the daemon listens, so a port that binds\n * here binds there.\n */\nexport function findFreePort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const server = net.createServer();\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n if (address === null || typeof address === \"string\") {\n server.close(() => reject(new Error(\"Could not determine a free port.\")));\n\n return;\n }\n const { port } = address;\n server.close(() => resolve(port));\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqBA,IAAa,aAAa;;AAE1B,IAAa,gBAAgB;;AAE7B,IAAa,kBAAkB;;;;;;;;;;AAU/B,IAAa,kBAAkB;AAmC/B,SAAgB,SAAS,aAA6B;CAClD,OAAO,KAAK,KAAK,aAAa,UAAU;AAC5C;AAEA,SAAgB,QAAQ,aAA6B;CACjD,OAAO,KAAK,KAAK,SAAS,WAAW,GAAG,aAAa;AACzD;;;;;;;;AASA,SAAgB,kBAAkB,aAAqB,KAAqB;CACxE,OAAO,KAAK,KAAK,SAAS,WAAW,GAAG,GAAG,gBAAgB,kBAAkB,GAAG,CAAC,CAAC,YAAY,GAAG;AACrG;AAEA,SAAgB,UAAU,aAA6B;CACnD,OAAO,KAAK,KAAK,SAAS,WAAW,GAAG,eAAe;AAC3D;AAEA,SAAgB,cAAc,aAA6B;CACvD,OAAO,KAAK,KAAK,SAAS,WAAW,GAAG,eAAe;AAC3D;;;;;;;;;;;;;AAcA,SAAgB,iBAAiB,aAAqB,cAA+B;CACjF,MAAM,SAAS,cAAc,WAAW;CACxC,GAAG,UAAU,SAAS,WAAW,GAAG,EAAE,WAAW,KAAK,CAAC;CAEvD,MAAM,gBAAyB;EAC3B,IAAI;GACA,MAAM,SAAS,GAAG,SAAS,QAAQ,IAAI;GACvC,GAAG,UAAU,QAAQ,GAAG,QAAQ,IAAI,oBAAG,IAAI,KAAK,EAAA,CAAE,YAAY,EAAE,GAAG;GACnE,GAAG,UAAU,MAAM;GAEnB,OAAO;EACX,SAAS,OAAO;GACZ,IAAK,MAAgC,SAAS,UAAU,MAAM;GAE9D,OAAO;EACX;CACJ;CAEA,IAAI,QAAQ,GAAG,OAAO;CAEtB,IAAI;EAMA,IADY,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,SAAS,MAAM,CAAC,CAAC,OACrD,IAAM,cAAc,OAAO;EAC/B,GAAG,WAAW,MAAM;CACxB,QAAQ,CAGR;CAEA,OAAO,QAAQ;AACnB;AAEA,SAAgB,iBAAiB,aAA2B;CACxD,IAAI;EACA,GAAG,WAAW,cAAc,WAAW,CAAC;CAC5C,QAAQ,CAER;AACJ;;;;;;;;AASA,SAAgB,UAAU,aAAyC;CAC/D,IAAI;CACJ,IAAI;EACA,MAAM,GAAG,aAAa,UAAU,WAAW,GAAG,MAAM;CACxD,QAAQ;EACJ,OAAO;CACX;CAEA,IAAI;EACA,MAAM,SAAS,KAAK,MAAM,GAAG;EAC7B,IACI,OAAO,OAAO,SAAS,YACvB,CAAC,OAAO,UAAU,OAAO,IAAI,KAC7B,OAAO,QAAQ,KACf,OAAO,OAAO,SACd,OAAO,OAAO,QAAQ,YACtB,OAAO,OAAO,YAAY,YAC1B,OAAO,OAAO,UAAU,YACxB,OAAO,MAAM,WAAW,KACxB,OAAO,OAAO,iBAAiB,YAC/B,CAAC,OAAO,UAAU,OAAO,YAAY,KACrC,OAAO,gBAAgB,KACvB,OAAO,eAAe,OAEtB,OAAO;EAKX,MAAM,YAA+D,CAAC;EACtE,IAAI,OAAO,aAAa,OAAO,OAAO,cAAc;QAC3C,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,SAAS,GACtD,IACI,SAAS,OAAO,MAAM,SAAS,YAAY,OAAO,UAAU,MAAM,IAAI,KACnE,MAAM,OAAO,KAAK,MAAM,QAAQ,SAAS,OAAO,MAAM,YAAY,UAErE,UAAU,OAAO;IAAE,MAAM,MAAM;IAAM,SAAS,MAAM;GAAQ;EAAA;EAKxE,OAAO;GACH,MAAM,OAAO;GACb,KAAK,OAAO;GACZ,SAAS,OAAO;GAChB,WAAW,OAAO,OAAO,cAAc,WAAW,OAAO,YAAY;GACrE,OAAO,OAAO;GACd,cAAc,OAAO;GACrB,GAAI,OAAO,KAAK,SAAS,CAAC,CAAC,SAAS,IAAI,EAAE,UAAU,IAAI,CAAC;EAC7D;CACJ,QAAQ;EACJ,OAAO;CACX;AACJ;AAEA,SAAgB,WAAW,aAAqB,OAA0B;CACtE,GAAG,UAAU,SAAS,WAAW,GAAG,EAAE,WAAW,KAAK,CAAC;CAGvD,MAAM,SAAS,UAAU,WAAW;CACpC,MAAM,YAAY,GAAG,OAAO,GAAG,QAAQ,IAAI;CAC3C,GAAG,cAAc,WAAW,GAAG,KAAK,UAAU,OAAO,MAAM,CAAC,EAAE,KAAK,MAAM;CACzE,GAAG,WAAW,WAAW,MAAM;AACnC;AAEA,SAAgB,WAAW,aAA2B;CAClD,IAAI;EACA,GAAG,WAAW,UAAU,WAAW,CAAC;CACxC,QAAQ,CAER;AACJ;;AAGA,SAAgB,WAAW,KAAsB;CAC7C,IAAI;EAGA,QAAQ,KAAK,KAAK,CAAC;EAEnB,OAAO;CACX,SAAS,OAAO;EAGZ,OAAQ,MAAgC,SAAS;CACrD;AACJ;;;;;;;;;AA2BA,SAAgB,eAAgC;CAC5C,OAAO,IAAI,SAAS,SAAS,WAAW;EACpC,MAAM,SAAS,IAAI,aAAa;EAChC,OAAO,KAAK,SAAS,MAAM;EAC3B,OAAO,OAAO,GAAG,mBAAmB;GAChC,MAAM,UAAU,OAAO,QAAQ;GAC/B,IAAI,YAAY,QAAQ,OAAO,YAAY,UAAU;IACjD,OAAO,YAAY,uBAAO,IAAI,MAAM,kCAAkC,CAAC,CAAC;IAExE;GACJ;GACA,MAAM,EAAE,SAAS;GACjB,OAAO,YAAY,QAAQ,IAAI,CAAC;EACpC,CAAC;CACL,CAAC;AACL"}
|
package/dist/utils/args.d.ts
CHANGED
|
@@ -53,6 +53,24 @@ export interface ParsedCommand<S extends arg.Spec> {
|
|
|
53
53
|
}
|
|
54
54
|
/** Did the line ask for help? Answered before dispatch, never by a handler. */
|
|
55
55
|
export declare function wantsHelp(rawArgs: string[]): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* The command line was wrong — nothing ran, and there is no stack to want.
|
|
58
|
+
*
|
|
59
|
+
* `bin/rebase.js` ends every failure with "Re-run with --debug for the stack
|
|
60
|
+
* trace.", which is right when something broke inside a command and absurd for
|
|
61
|
+
* a typo: the stack points at `arg` and this file, neither of which the person
|
|
62
|
+
* who mistyped `--ouput` has any interest in. Worse, `--debug` is a flag, so
|
|
63
|
+
* the suggested next command is *another* command line — and the one thing we
|
|
64
|
+
* have just established is that the command line is what went wrong.
|
|
65
|
+
*
|
|
66
|
+
* Marked with a property rather than left to `instanceof`, because the reader
|
|
67
|
+
* is `bin/rebase.js`: it imports the bundle, so the class object it would test
|
|
68
|
+
* against is not the one this file compiled to.
|
|
69
|
+
*/
|
|
70
|
+
export declare class UsageError extends Error {
|
|
71
|
+
readonly isUsageError = true;
|
|
72
|
+
constructor(message: string);
|
|
73
|
+
}
|
|
56
74
|
/**
|
|
57
75
|
* Resolve a command's flags and positionals from the full `process.argv`.
|
|
58
76
|
*
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The words that name a command, with the flags taken back out.
|
|
3
|
+
*
|
|
4
|
+
* `rawArgs` is the whole of `process.argv`, and reading the command's words at
|
|
5
|
+
* fixed indices off it assumes nothing precedes them. Something routinely does:
|
|
6
|
+
* `--debug` is what `bin/rebase.js` prints after every failure as the thing to
|
|
7
|
+
* re-run with, so it is the single most likely token to appear before a command
|
|
8
|
+
* word, and it shifted every index by one.
|
|
9
|
+
*
|
|
10
|
+
* What that cost is not a bad error message. `rebase db branch switch feature`
|
|
11
|
+
* writes a per-checkout pointer that the CLI owns; the driver, running as a
|
|
12
|
+
* child process, cannot persist it and does not try. The dispatch found
|
|
13
|
+
* `switch` by position, so `rebase --debug db branch switch feature` missed the
|
|
14
|
+
* CLI's branch and handed the line to the driver — which reported success and
|
|
15
|
+
* left the checkout on the main database. Every subsequent `dev`, `push` and
|
|
16
|
+
* `backup` then ran against the wrong database, believing it was the branch,
|
|
17
|
+
* which is the exact failure branching exists to prevent.
|
|
18
|
+
*
|
|
19
|
+
* The words are anchored on the command name rather than taken from position
|
|
20
|
+
* zero, because a flag written in the space form (`--database-url <url>`) leaves
|
|
21
|
+
* its value behind as a bare token and no parser at this level knows which
|
|
22
|
+
* flags take values. Anchoring survives that: the value would have to be the
|
|
23
|
+
* literal command name to fool it.
|
|
24
|
+
*
|
|
25
|
+
* Returns `[]` when the command name is not on the line at all.
|
|
26
|
+
*/
|
|
27
|
+
export declare function commandWords(rawArgs: readonly string[], command: string): string[];
|
|
28
|
+
/**
|
|
29
|
+
* The line to hand a spawned driver, starting at the command word.
|
|
30
|
+
*
|
|
31
|
+
* The same defect one layer down, and it does not degrade gracefully: the
|
|
32
|
+
* driver reads its domain from `args[0]`, so `rebase --debug db push` spawned
|
|
33
|
+
* it with `["--debug", "db", "push"]` and it answered "Unknown domain command:
|
|
34
|
+
* --debug" — for a flag the CLI itself tells you to add after any failure.
|
|
35
|
+
*
|
|
36
|
+
* Tokens written *before* the command word are moved after it rather than
|
|
37
|
+
* dropped. Dropping them would be the same class of bug in the other direction:
|
|
38
|
+
* `rebase --database-url postgres://… db push` would silently lose the flag that
|
|
39
|
+
* says which database to touch. Appending keeps each flag next to its value, and
|
|
40
|
+
* `arg` reads flags wherever they appear.
|
|
41
|
+
*
|
|
42
|
+
* Falls back to `rawArgs.slice(2)` when the command word is not on the line,
|
|
43
|
+
* which is what an internally synthesised argv looks like.
|
|
44
|
+
*/
|
|
45
|
+
export declare function argsFromCommand(rawArgs: readonly string[], command: string): string[];
|
|
@@ -18,6 +18,11 @@ export type PreflightOutcome = {
|
|
|
18
18
|
} | {
|
|
19
19
|
action: "start-failed";
|
|
20
20
|
hint: string;
|
|
21
|
+
} | {
|
|
22
|
+
action: "wrong-port";
|
|
23
|
+
hint: string;
|
|
24
|
+
port: number;
|
|
25
|
+
composePort: number;
|
|
21
26
|
} | {
|
|
22
27
|
action: "started";
|
|
23
28
|
port: number;
|
|
@@ -43,6 +48,40 @@ export declare function parseLoopbackDsn(dsn: string | undefined): {
|
|
|
43
48
|
* manual steps, which are correct.
|
|
44
49
|
*/
|
|
45
50
|
export declare function composeDeclaresDbService(yamlText: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The connection string the compose file's `db` service will answer on.
|
|
53
|
+
*
|
|
54
|
+
* `--docker` used to be a flag that changed a banner and nothing else: it
|
|
55
|
+
* resolved to `kind: "docker"`, and every caller then had no URL to reach the
|
|
56
|
+
* container with, so the backend booted with no `DATABASE_URL` at all and died
|
|
57
|
+
* on the message a project with no database gets. The container it had just
|
|
58
|
+
* been asked for was never started, because the preflight that starts it needs
|
|
59
|
+
* a DSN to decide the database is local and not running.
|
|
60
|
+
*
|
|
61
|
+
* So the URL is derived rather than required: the compose file names the user,
|
|
62
|
+
* the database and the published host port, and `.env` holds the password
|
|
63
|
+
* compose itself interpolates. That is the same string `rebase init` writes as
|
|
64
|
+
* the commented-out `DATABASE_URL`, which is what makes uncommenting that line
|
|
65
|
+
* and passing `--docker` reach the same database.
|
|
66
|
+
*
|
|
67
|
+
* Returns null when the file declares no `db` service, or declares one this
|
|
68
|
+
* scan cannot read — a `--docker` that cannot be honoured has to say so, and
|
|
69
|
+
* a guess would point at somebody else's Postgres on 5432.
|
|
70
|
+
*
|
|
71
|
+
* Same text scan as {@link composeDeclaresDbService}, for the same reason.
|
|
72
|
+
*/
|
|
73
|
+
export declare function composeDatabaseUrl(yamlText: string, env?: Record<string, string | undefined>): string | null;
|
|
74
|
+
/**
|
|
75
|
+
* The host port the compose `db` service publishes, or null.
|
|
76
|
+
*
|
|
77
|
+
* Separate from {@link composeDatabaseUrl} because the port is answerable on
|
|
78
|
+
* its own: the URL needs the user, the password and the database name too, and
|
|
79
|
+
* a compose file missing any of them still says which port a container would
|
|
80
|
+
* listen on. That is the question "would starting this container answer the
|
|
81
|
+
* DSN in `.env`?" — see the `wrong-port` branch of {@link ensureDevDatabase},
|
|
82
|
+
* where getting it wrong meant starting a container nobody asked for.
|
|
83
|
+
*/
|
|
84
|
+
export declare function composeHostPort(yamlText: string): number | null;
|
|
46
85
|
/** Is something accepting connections there right now? */
|
|
47
86
|
export declare function probeTcp(host: string, port: number, timeoutMs?: number): Promise<boolean>;
|
|
48
87
|
/**
|
|
@@ -64,6 +103,13 @@ export interface EnsureDevDatabaseOptions {
|
|
|
64
103
|
hasCollections: boolean;
|
|
65
104
|
/** Runs `rebase db push` for this project. Injected so tests need no database. */
|
|
66
105
|
pushSchema: () => Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Brings the compose `db` service up. Injected for the same reason
|
|
108
|
+
* `pushSchema` is: what happens *after* a start — a push that fails without
|
|
109
|
+
* taking the dev server with it — is the part worth asserting, and it was
|
|
110
|
+
* untestable while the only way in was a real `docker compose up`.
|
|
111
|
+
*/
|
|
112
|
+
startDatabase?: (projectRoot: string) => Promise<void>;
|
|
67
113
|
log?: (message: string) => void;
|
|
68
114
|
}
|
|
69
115
|
/**
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two package managers the CLI can scaffold for.
|
|
3
|
+
*
|
|
4
|
+
* Yarn and bun are deliberately absent, and this is the note that says so
|
|
5
|
+
* rather than leaving it to be discovered. A yarn or bun user gets an npm
|
|
6
|
+
* project: `detectPackageManager` finds no `pnpm-lock.yaml` and no
|
|
7
|
+
* `package-lock.json`, falls through to the pnpm probe, and lands on npm —
|
|
8
|
+
* silently, and with a `yarn.lock` or `bun.lockb` sitting right there in the
|
|
9
|
+
* directory it was asked about.
|
|
10
|
+
*
|
|
11
|
+
* The reason is not indifference. Every generated project is a workspace, and
|
|
12
|
+
* the scaffold writes one protocol for the workspace links —
|
|
13
|
+
* `workspaceProtocol` below is `"workspace:*"` or `"*"`. Yarn understands
|
|
14
|
+
* `workspace:*` and bun does not; the two also disagree with pnpm about where
|
|
15
|
+
* `node_modules` goes, which is what `rebase dev` walks to find the backend.
|
|
16
|
+
* Supporting them means a fourth and fifth scaffold shape, each needing its own
|
|
17
|
+
* end-to-end test, and neither exists.
|
|
18
|
+
*
|
|
19
|
+
* So the honest position is: pnpm (recommended) or npm. A yarn or bun user is
|
|
20
|
+
* better served knowing they will get an npm workspace than finding out from
|
|
21
|
+
* the lockfile that appears after their first install. See
|
|
22
|
+
* {@link detectPackageManager}, which now says so out loud.
|
|
23
|
+
*/
|
|
1
24
|
export type PackageManager = "pnpm" | "npm";
|
|
2
25
|
export interface PMCommands {
|
|
3
26
|
/** The binary name ("pnpm" | "npm"). */
|