@rebasepro/cli 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +19 -3
  2. package/bin/rebase.js +1 -1
  3. package/dist/{branch-pointer-dBiClJ0p.js → branch-pointer-kk9F5FBO.js} +2 -2
  4. package/dist/{branch-pointer-dBiClJ0p.js.map → branch-pointer-kk9F5FBO.js.map} +1 -1
  5. package/dist/bundle.d.ts +6 -2
  6. package/dist/cli.d.ts +21 -0
  7. package/dist/commands/__test_support__/parser-specs.d.ts +13 -0
  8. package/dist/commands/agent-setup.d.ts +116 -0
  9. package/dist/commands/api-keys.d.ts +84 -23
  10. package/dist/commands/apps.d.ts +17 -0
  11. package/dist/commands/auth.d.ts +7 -0
  12. package/dist/commands/build.d.ts +5 -1
  13. package/dist/commands/cloud/auth.d.ts +4 -3
  14. package/dist/commands/cloud/bundle-deploy.d.ts +45 -0
  15. package/dist/commands/cloud/context.d.ts +44 -5
  16. package/dist/commands/cloud/cron.d.ts +53 -0
  17. package/dist/commands/cloud/debug.d.ts +134 -0
  18. package/dist/commands/cloud/index.d.ts +89 -12
  19. package/dist/commands/cloud/token-capabilities.d.ts +76 -0
  20. package/dist/commands/cloud/tokens.d.ts +23 -0
  21. package/dist/commands/db.d.ts +15 -1
  22. package/dist/commands/dev.d.ts +78 -7
  23. package/dist/commands/doctor.d.ts +5 -0
  24. package/dist/commands/generate_sdk.d.ts +3 -2
  25. package/dist/commands/init.d.ts +57 -14
  26. package/dist/commands/schema.d.ts +6 -0
  27. package/dist/commands/skills.d.ts +109 -5
  28. package/dist/commands/start.d.ts +29 -0
  29. package/dist/{daemon-DR9Tk0La.js → daemon-C8p32i-n.js} +5 -3
  30. package/dist/daemon-C8p32i-n.js.map +1 -0
  31. package/dist/dev-db/daemon.d.ts +10 -0
  32. package/dist/dev-db/prepare.d.ts +10 -0
  33. package/dist/fold-static.d.ts +26 -3
  34. package/dist/index.es.js +12086 -9459
  35. package/dist/index.es.js.map +1 -1
  36. package/dist/manifest.d.ts +41 -1
  37. package/dist/package-manager-CXQevsE1.js +193 -0
  38. package/dist/package-manager-CXQevsE1.js.map +1 -0
  39. package/dist/{project-3WCbrrfW.js → project-DmaGLmH2.js} +4 -183
  40. package/dist/project-DmaGLmH2.js.map +1 -0
  41. package/dist/upgrade.d.ts +14 -2
  42. package/dist/utils/path-flags.d.ts +18 -0
  43. package/dist/version-DsgBmP22.js +58 -0
  44. package/dist/version-DsgBmP22.js.map +1 -0
  45. package/package.json +7 -7
  46. package/templates/eject/backend/src/index.ts +14 -1
  47. package/templates/overlays/baas/ai-instructions.md +1 -1
  48. package/templates/overlays/baas/config/storage.ts +5 -3
  49. package/templates/template/.cursorrules +1 -1
  50. package/templates/template/.env.example +10 -6
  51. package/templates/template/.github/copilot-instructions.md +1 -1
  52. package/templates/template/.mcp.json +2 -2
  53. package/templates/template/.windsurfrules +1 -1
  54. package/templates/template/AGENTS.md +1 -1
  55. package/templates/template/CLAUDE.md +1 -1
  56. package/templates/template/README.md +10 -11
  57. package/templates/template/ai-instructions.md +3 -3
  58. package/templates/template/backend/functions/hello.ts +8 -0
  59. package/templates/template/backend/package.json +1 -1
  60. package/templates/template/config/storage.ts +5 -3
  61. package/templates/template/docker-compose.yml +6 -6
  62. package/templates/template/gitignore +6 -1
  63. package/templates/template/package.json +4 -3
  64. package/templates/template/scripts/example.ts +4 -3
  65. package/dist/daemon-DR9Tk0La.js.map +0 -1
  66. package/dist/project-3WCbrrfW.js.map +0 -1
package/README.md CHANGED
@@ -22,7 +22,7 @@ The CLI is also bundled with every Rebase project as a local dependency.
22
22
  | `rebase dev` | Start the development server (backend + frontend) |
23
23
  | `rebase build` | Build the apps declared in `rebase.json` into a bundle |
24
24
  | `rebase normalize-imports` | Complete compiled output's relative imports for Node ESM |
25
- | `rebase start` | Start the backend server (production) |
25
+ | `rebase start` | Run the built bundle, as a deployment does, in the `NODE_ENV` you set |
26
26
  | `rebase apps list` | Show the apps this repository declares |
27
27
  | `rebase schema generate` | Generate Drizzle schema from collection definitions |
28
28
  | `rebase schema introspect` | Introspect an existing database → Rebase collections |
@@ -36,7 +36,7 @@ The CLI is also bundled with every Rebase project as a local dependency.
36
36
  | `rebase db reset` | Delete the managed development database and start over |
37
37
  | `rebase generate-sdk` | Generate a typed TypeScript SDK from collections |
38
38
  | `rebase auth reset-password` | Reset a user's password |
39
- | `rebase api-keys list \| create \| revoke` | Manage scoped service API keys |
39
+ | `rebase api-keys list \| get \| create \| revoke \| scopes` | Manage service API keys and the scopes they hold |
40
40
  | `rebase doctor` | Detect schema drift between collections, Drizzle schema, and database |
41
41
  | `rebase status` | Show every resource this project declares and whether its variables are set |
42
42
  | `rebase resources` | List the databases, buckets and topics this project declares |
@@ -64,7 +64,7 @@ rebase cloud status # project status at a glance
64
64
 
65
65
  | Group | Commands |
66
66
  |-------|----------|
67
- | Auth | `login`, `logout`, `whoami` |
67
+ | Auth | `login`, `logout`, `whoami`, `tokens list \| create \| revoke` |
68
68
  | Link/context | `link`, `unlink`, `use [org]`, `open` |
69
69
  | Projects | `projects list \| create \| info \| delete` |
70
70
  | Deploy/observe | `deploy`, `logs [--runtime] [-f]`, `status`, `metrics` |
@@ -75,6 +75,22 @@ rebase cloud status # project status at a glance
75
75
  Most commands act on the linked project unless you pass `--project <id>`.
76
76
  Run `rebase cloud --help` for the full list.
77
77
 
78
+ ### Tokens for CI and agents
79
+
80
+ A token acts as your account, narrowed to what it was created for on one project.
81
+ Create it from a signed-in terminal, then hand it to CI as `REBASE_TOKEN` — every
82
+ `rebase cloud` command uses it instead of a login, and never reads or writes the
83
+ stored session:
84
+
85
+ ```bash
86
+ rebase cloud tokens create --project shop --can deploy,logs --expires-in 90
87
+ # → export REBASE_TOKEN=rk_live_… (shown once)
88
+ REBASE_TOKEN=rk_live_… rebase cloud deploy --project shop
89
+ ```
90
+
91
+ `--can` takes `deploy`, `logs`, `env`, `database` and `backups`. A token cannot
92
+ create, list or revoke tokens, and none can restore a backup.
93
+
78
94
  ## Quick Start
79
95
 
80
96
  ```bash
package/bin/rebase.js CHANGED
@@ -133,7 +133,7 @@ function warnIfStale() {
133
133
  : seconds >= 60 ? `${Math.round(seconds / 60)}m` : `${seconds}s`;
134
134
  process.stderr.write(
135
135
  `${yellow(`⚠ rebase CLI: dist/ is ${ago} older than src/ — you are running a stale build.`)}\n` +
136
- ` Rebuild with: (cd ${join(here, "..")} && npm run build)\n`
136
+ " Rebuild with: pnpm --filter @rebasepro/cli build\n"
137
137
  );
138
138
  }
139
139
 
@@ -106,6 +106,6 @@ function databaseNameOf(connectionString) {
106
106
  }
107
107
  }
108
108
  //#endregion
109
- export { branch_pointer_exports as n, readActiveBranch as r, branchUrl as t };
109
+ export { readActiveBranch as i, branch_pointer_exports as n, databaseNameOf as r, branchUrl as t };
110
110
 
111
- //# sourceMappingURL=branch-pointer-dBiClJ0p.js.map
111
+ //# sourceMappingURL=branch-pointer-kk9F5FBO.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"branch-pointer-dBiClJ0p.js","names":[],"sources":["../src/dev-db/branch-pointer.ts"],"sourcesContent":["/**\n * Which database branch this checkout is working on.\n *\n * `rebase db branch create` made a full copy of the database in about a second\n * and then stopped one step short of being a feature: there was no `switch`, no\n * `--branch` on `rebase dev`, and nothing wrote the branch anywhere a command\n * would read it. The only way to *use* a branch was to hand-edit\n * `DATABASE_URL`, and the documentation claimed otherwise — \"the CLI updates\n * your local development configuration\" — which it did not; the `.env` was\n * byte-identical afterwards.\n *\n * This is that missing step, and the shape of it is chosen to avoid three\n * things:\n *\n * 1. **It does not touch `.env`.** That file holds secrets, comments and the\n * developer's own ordering, and a command that rewrites it in place will\n * eventually lose something that was not its business. The branch is\n * recorded beside the other per-checkout state in `.rebase/`, which is\n * already gitignored — a branch is a fact about one machine, never about\n * the project.\n *\n * 2. **It stores a name, not a connection string.** The credentials stay in\n * exactly one place; the URL is derived by swapping the database name on\n * the base one at resolution time. A stored URL would be a second copy of\n * the password, and would go stale the moment `.env` changed.\n *\n * 3. **It does not outrank an explicit instruction.** `--database-url` and a\n * `DATABASE_URL` in the shell are more immediate and more specific than a\n * switch made yesterday, so they still win. The pointer sits directly above\n * the project's `.env`: it is a deliberate, persistent choice, and `.env` is\n * the project default it is meant to override. Anything less and `switch`\n * would silently do nothing on every project that sets `DATABASE_URL` —\n * which is every project not using the managed database.\n */\n\nimport fs from \"fs\";\nimport path from \"path\";\n\n/** Where the pointer lives, next to the rest of the per-checkout state. */\nexport function branchPointerPath(projectRoot: string): string {\n return path.join(projectRoot, \".rebase\", \"branch.json\");\n}\n\nexport interface ActiveBranch {\n /** The name the developer typed. */\n name: string;\n /** The PostgreSQL database that name refers to. */\n database: string;\n}\n\n/**\n * The prefix `BranchService` puts on every branch database.\n *\n * Duplicated from the driver rather than imported because the CLI must answer\n * \"which database\" without loading a database driver — `rebase dev` reads this\n * before it knows which plugin the project uses. It is covered by a test that\n * fails if the driver's prefix moves.\n */\nexport const BRANCH_DB_PREFIX = \"rb_\";\n\nexport function branchDatabaseName(name: string): string {\n return `${BRANCH_DB_PREFIX}${name}`;\n}\n\n/** Read the active branch, or null when the checkout is on the main database. */\nexport function readActiveBranch(projectRoot: string): ActiveBranch | null {\n try {\n const raw = fs.readFileSync(branchPointerPath(projectRoot), \"utf-8\");\n const parsed = JSON.parse(raw) as Partial<ActiveBranch>;\n if (typeof parsed.name !== \"string\" || typeof parsed.database !== \"string\") return null;\n if (!parsed.name || !parsed.database) return null;\n\n return { name: parsed.name, database: parsed.database };\n } catch {\n // Absent is the common case and is not an error. A corrupt file is\n // treated the same way: the answer is \"no branch\", which is the state\n // the developer can always get back to with `branch switch --off`.\n return null;\n }\n}\n\nexport function writeActiveBranch(projectRoot: string, branch: ActiveBranch): void {\n const file = branchPointerPath(projectRoot);\n fs.mkdirSync(path.dirname(file), { recursive: true });\n fs.writeFileSync(file, `${JSON.stringify(branch, null, 2)}\\n`, \"utf-8\");\n}\n\n/** Return to the main database. Absent is success — this is idempotent. */\nexport function clearActiveBranch(projectRoot: string): void {\n fs.rmSync(branchPointerPath(projectRoot), { force: true });\n}\n\n/**\n * The base connection string with its database swapped for the branch's.\n *\n * Everything else is carried across untouched — credentials, host, port, and\n * the query string, which is load-bearing here: `?sslmode=disable` is what a\n * local Docker Postgres needs, and dropping it turns a switch into a TLS error\n * that says nothing about branches.\n *\n * Returns null for a connection string that cannot be parsed, so the caller can\n * say so plainly rather than emitting a mangled URL that fails later.\n */\nexport function branchUrl(baseUrl: string, database: string): string | null {\n try {\n const url = new URL(baseUrl);\n // `pathname` keeps its leading slash; a URL with no database at all\n // still gets one, which is what connecting to a named branch means.\n url.pathname = `/${database}`;\n\n return url.toString();\n } catch {\n return null;\n }\n}\n\n/** The database name in a connection string, for reporting what you are on. */\nexport function databaseNameOf(connectionString: string): string | null {\n try {\n const name = new URL(connectionString).pathname.replace(/^\\//, \"\");\n\n return name || null;\n } catch {\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,kBAAkB,aAA6B;CAC3D,OAAO,KAAK,KAAK,aAAa,WAAW,aAAa;AAC1D;AAmBA,SAAgB,mBAAmB,MAAsB;CACrD,OAAO,MAAsB;AACjC;;AAGA,SAAgB,iBAAiB,aAA0C;CACvE,IAAI;EACA,MAAM,MAAM,GAAG,aAAa,kBAAkB,WAAW,GAAG,OAAO;EACnE,MAAM,SAAS,KAAK,MAAM,GAAG;EAC7B,IAAI,OAAO,OAAO,SAAS,YAAY,OAAO,OAAO,aAAa,UAAU,OAAO;EACnF,IAAI,CAAC,OAAO,QAAQ,CAAC,OAAO,UAAU,OAAO;EAE7C,OAAO;GAAE,MAAM,OAAO;GAAM,UAAU,OAAO;EAAS;CAC1D,QAAQ;EAIJ,OAAO;CACX;AACJ;AAEA,SAAgB,kBAAkB,aAAqB,QAA4B;CAC/E,MAAM,OAAO,kBAAkB,WAAW;CAC1C,GAAG,UAAU,KAAK,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;CACpD,GAAG,cAAc,MAAM,GAAG,KAAK,UAAU,QAAQ,MAAM,CAAC,EAAE,KAAK,OAAO;AAC1E;;AAGA,SAAgB,kBAAkB,aAA2B;CACzD,GAAG,OAAO,kBAAkB,WAAW,GAAG,EAAE,OAAO,KAAK,CAAC;AAC7D;;;;;;;;;;;;AAaA,SAAgB,UAAU,SAAiB,UAAiC;CACxE,IAAI;EACA,MAAM,MAAM,IAAI,IAAI,OAAO;EAG3B,IAAI,WAAW,IAAI;EAEnB,OAAO,IAAI,SAAS;CACxB,QAAQ;EACJ,OAAO;CACX;AACJ;;AAGA,SAAgB,eAAe,kBAAyC;CACpE,IAAI;EAGA,OAFa,IAAI,IAAI,gBAAgB,CAAC,CAAC,SAAS,QAAQ,OAAO,EAExD,KAAQ;CACnB,QAAQ;EACJ,OAAO;CACX;AACJ"}
1
+ {"version":3,"file":"branch-pointer-kk9F5FBO.js","names":[],"sources":["../src/dev-db/branch-pointer.ts"],"sourcesContent":["/**\n * Which database branch this checkout is working on.\n *\n * `rebase db branch create` made a full copy of the database in about a second\n * and then stopped one step short of being a feature: there was no `switch`, no\n * `--branch` on `rebase dev`, and nothing wrote the branch anywhere a command\n * would read it. The only way to *use* a branch was to hand-edit\n * `DATABASE_URL`, and the documentation claimed otherwise — \"the CLI updates\n * your local development configuration\" — which it did not; the `.env` was\n * byte-identical afterwards.\n *\n * This is that missing step, and the shape of it is chosen to avoid three\n * things:\n *\n * 1. **It does not touch `.env`.** That file holds secrets, comments and the\n * developer's own ordering, and a command that rewrites it in place will\n * eventually lose something that was not its business. The branch is\n * recorded beside the other per-checkout state in `.rebase/`, which is\n * already gitignored — a branch is a fact about one machine, never about\n * the project.\n *\n * 2. **It stores a name, not a connection string.** The credentials stay in\n * exactly one place; the URL is derived by swapping the database name on\n * the base one at resolution time. A stored URL would be a second copy of\n * the password, and would go stale the moment `.env` changed.\n *\n * 3. **It does not outrank an explicit instruction.** `--database-url` and a\n * `DATABASE_URL` in the shell are more immediate and more specific than a\n * switch made yesterday, so they still win. The pointer sits directly above\n * the project's `.env`: it is a deliberate, persistent choice, and `.env` is\n * the project default it is meant to override. Anything less and `switch`\n * would silently do nothing on every project that sets `DATABASE_URL` —\n * which is every project not using the managed database.\n */\n\nimport fs from \"fs\";\nimport path from \"path\";\n\n/** Where the pointer lives, next to the rest of the per-checkout state. */\nexport function branchPointerPath(projectRoot: string): string {\n return path.join(projectRoot, \".rebase\", \"branch.json\");\n}\n\nexport interface ActiveBranch {\n /** The name the developer typed. */\n name: string;\n /** The PostgreSQL database that name refers to. */\n database: string;\n}\n\n/**\n * The prefix `BranchService` puts on every branch database.\n *\n * Duplicated from the driver rather than imported because the CLI must answer\n * \"which database\" without loading a database driver — `rebase dev` reads this\n * before it knows which plugin the project uses. It is covered by a test that\n * fails if the driver's prefix moves.\n */\nexport const BRANCH_DB_PREFIX = \"rb_\";\n\nexport function branchDatabaseName(name: string): string {\n return `${BRANCH_DB_PREFIX}${name}`;\n}\n\n/** Read the active branch, or null when the checkout is on the main database. */\nexport function readActiveBranch(projectRoot: string): ActiveBranch | null {\n try {\n const raw = fs.readFileSync(branchPointerPath(projectRoot), \"utf-8\");\n const parsed = JSON.parse(raw) as Partial<ActiveBranch>;\n if (typeof parsed.name !== \"string\" || typeof parsed.database !== \"string\") return null;\n if (!parsed.name || !parsed.database) return null;\n\n return { name: parsed.name, database: parsed.database };\n } catch {\n // Absent is the common case and is not an error. A corrupt file is\n // treated the same way: the answer is \"no branch\", which is the state\n // the developer can always get back to with `branch switch --off`.\n return null;\n }\n}\n\nexport function writeActiveBranch(projectRoot: string, branch: ActiveBranch): void {\n const file = branchPointerPath(projectRoot);\n fs.mkdirSync(path.dirname(file), { recursive: true });\n fs.writeFileSync(file, `${JSON.stringify(branch, null, 2)}\\n`, \"utf-8\");\n}\n\n/** Return to the main database. Absent is success — this is idempotent. */\nexport function clearActiveBranch(projectRoot: string): void {\n fs.rmSync(branchPointerPath(projectRoot), { force: true });\n}\n\n/**\n * The base connection string with its database swapped for the branch's.\n *\n * Everything else is carried across untouched — credentials, host, port, and\n * the query string, which is load-bearing here: `?sslmode=disable` is what a\n * local Docker Postgres needs, and dropping it turns a switch into a TLS error\n * that says nothing about branches.\n *\n * Returns null for a connection string that cannot be parsed, so the caller can\n * say so plainly rather than emitting a mangled URL that fails later.\n */\nexport function branchUrl(baseUrl: string, database: string): string | null {\n try {\n const url = new URL(baseUrl);\n // `pathname` keeps its leading slash; a URL with no database at all\n // still gets one, which is what connecting to a named branch means.\n url.pathname = `/${database}`;\n\n return url.toString();\n } catch {\n return null;\n }\n}\n\n/** The database name in a connection string, for reporting what you are on. */\nexport function databaseNameOf(connectionString: string): string | null {\n try {\n const name = new URL(connectionString).pathname.replace(/^\\//, \"\");\n\n return name || null;\n } catch {\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,kBAAkB,aAA6B;CAC3D,OAAO,KAAK,KAAK,aAAa,WAAW,aAAa;AAC1D;AAmBA,SAAgB,mBAAmB,MAAsB;CACrD,OAAO,MAAsB;AACjC;;AAGA,SAAgB,iBAAiB,aAA0C;CACvE,IAAI;EACA,MAAM,MAAM,GAAG,aAAa,kBAAkB,WAAW,GAAG,OAAO;EACnE,MAAM,SAAS,KAAK,MAAM,GAAG;EAC7B,IAAI,OAAO,OAAO,SAAS,YAAY,OAAO,OAAO,aAAa,UAAU,OAAO;EACnF,IAAI,CAAC,OAAO,QAAQ,CAAC,OAAO,UAAU,OAAO;EAE7C,OAAO;GAAE,MAAM,OAAO;GAAM,UAAU,OAAO;EAAS;CAC1D,QAAQ;EAIJ,OAAO;CACX;AACJ;AAEA,SAAgB,kBAAkB,aAAqB,QAA4B;CAC/E,MAAM,OAAO,kBAAkB,WAAW;CAC1C,GAAG,UAAU,KAAK,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;CACpD,GAAG,cAAc,MAAM,GAAG,KAAK,UAAU,QAAQ,MAAM,CAAC,EAAE,KAAK,OAAO;AAC1E;;AAGA,SAAgB,kBAAkB,aAA2B;CACzD,GAAG,OAAO,kBAAkB,WAAW,GAAG,EAAE,OAAO,KAAK,CAAC;AAC7D;;;;;;;;;;;;AAaA,SAAgB,UAAU,SAAiB,UAAiC;CACxE,IAAI;EACA,MAAM,MAAM,IAAI,IAAI,OAAO;EAG3B,IAAI,WAAW,IAAI;EAEnB,OAAO,IAAI,SAAS;CACxB,QAAQ;EACJ,OAAO;CACX;AACJ;;AAGA,SAAgB,eAAe,kBAAyC;CACpE,IAAI;EAGA,OAFa,IAAI,IAAI,gBAAgB,CAAC,CAAC,SAAS,QAAQ,OAAO,EAExD,KAAQ;CACnB,QAAQ;EACJ,OAAO;CACX;AACJ"}
package/dist/bundle.d.ts CHANGED
@@ -404,8 +404,10 @@ export declare function foldStaticIntoBundle(options: {
404
404
  assetsDir: string;
405
405
  /** The app's name in `rebase.json`. Names its directory inside the bundle. */
406
406
  appName: string;
407
- /** Public base path this app is served under. */
407
+ /** Public base path this app is served under. Always a path, never a URL. */
408
408
  path: string;
409
+ /** The one hostname this app answers on, when its manifest `path` names one. */
410
+ host?: string;
409
411
  /** Serve `index.html` for unmatched paths under `path`. */
410
412
  spa: boolean;
411
413
  /** Where this app mounts the Rebase CMS, if it does. */
@@ -420,8 +422,10 @@ export declare function buildStaticBundle(options: {
420
422
  assetsDir: string;
421
423
  outDir: string;
422
424
  runtimeRange: string;
423
- /** Public base path. Default `/` — a standalone bundle owns its origin. */
425
+ /** Public base path. Default `/` — a standalone bundle owns its origin. Always a path. */
424
426
  path?: string;
427
+ /** The one hostname this app answers on, when its manifest `path` names one. */
428
+ host?: string;
425
429
  /** Serve `index.html` for unmatched paths. Default `true`. */
426
430
  spa?: boolean;
427
431
  /** Where this app mounts the Rebase CMS, if it does. */
package/dist/cli.d.ts CHANGED
@@ -18,6 +18,27 @@ export declare const ROOT_FLAGS: {
18
18
  readonly "-v": "--version";
19
19
  readonly "-h": "--help";
20
20
  };
21
+ /**
22
+ * The subcommand in the words after a command, read against the subcommands the
23
+ * group dispatches.
24
+ *
25
+ * The first word that is not a flag is not always the subcommand: after a flag
26
+ * that takes a value it is that value. `rebase db --collections ./c push` was
27
+ * refused as an unknown db command `./c`, `rebase api-keys --name ci create` as
28
+ * one called `ci`, and `rebase skills --agent claude install` as `claude` —
29
+ * lines every one of those commands parses happily once it is reached.
30
+ *
31
+ * Which flags take a value is not known at this level, so the vocabulary decides
32
+ * instead: a word the group dispatches is the subcommand; a word straight after
33
+ * a flag (and not glued to it with `=`) may be that flag's value and is passed
34
+ * over. When no word is a known subcommand, the first word is returned as
35
+ * before, so a typo — `rebase db psuh`, `rebase db --dry-run psuh` — still
36
+ * reaches the group and is refused by name. A command with no vocabulary keeps
37
+ * the first word.
38
+ *
39
+ * Exported for its tests.
40
+ */
41
+ export declare function resolveSubcommand(after: readonly string[], known: readonly string[]): string | undefined;
21
42
  /**
22
43
  * The command and subcommand words as `cli.error` may send them: each one is a
23
44
  * word this CLI dispatches, `"none"` when nothing was typed, or `"other"`.
@@ -0,0 +1,13 @@
1
+ export interface ParsedCommandSpec {
2
+ /** The command words, as the call names them: `"build"`, `"skills install"`. */
3
+ command: string;
4
+ /** Canonical long flags: the value is a type, not another flag's name. */
5
+ flags: string[];
6
+ /** Every spelling the parser accepts, aliases and short flags included. */
7
+ accepted: string[];
8
+ file: string;
9
+ }
10
+ /** The parser calls in one file. Throws on a call it cannot read, so a new shape fails loudly. */
11
+ export declare function parsedCommands(file: string): ParsedCommandSpec[];
12
+ /** Every parser call under `src/` that the dispatch reaches: `cli.ts` and the command modules. */
13
+ export declare function allParsedCommands(commandsDir: string): ParsedCommandSpec[];
@@ -0,0 +1,116 @@
1
+ import { type AgentKey, type LoadedSkill } from "./skills.js";
2
+ import { type PackageManager } from "../utils/package-manager.js";
3
+ /** The name the server is registered under, in every config. */
4
+ export declare const MCP_SERVER_NAME = "rebase";
5
+ /** The MCP server's package: a devDependency of the project, pinned with the rest. */
6
+ export declare const MCP_PACKAGE = "@rebasepro/mcp";
7
+ /** The bin {@link MCP_PACKAGE} declares. */
8
+ export declare const MCP_BIN = "rebase-mcp";
9
+ /** The command an assistant spawns, and its arguments. */
10
+ export interface McpLaunch {
11
+ command: string;
12
+ args: string[];
13
+ }
14
+ /**
15
+ * How an assistant starts the Rebase MCP server: the project's own copy, through
16
+ * the project's package manager.
17
+ *
18
+ * Not `npx -y @rebasepro/mcp`, which fetched the newest release on every first
19
+ * start. The server drives the project's own CLI (`pnpm exec rebase …`) and calls
20
+ * the project's own backend (`/api/admin/schema/plan`, …), whose versions the
21
+ * lockfile pins — so a server from the registry ran ahead of both, at every
22
+ * start, and downloaded before it could answer. `@rebasepro/mcp` is a
23
+ * devDependency of the scaffold, pinned to the CLI's version like every other
24
+ * `@rebasepro` package, and this runs that copy.
25
+ *
26
+ * `npx --no`, not `npx`: an assistant spawns the server with no TTY, and npx
27
+ * then assumes "yes" and installs a missing package from the registry under the
28
+ * name it was given. `--no` makes a missing install an error instead.
29
+ */
30
+ export declare function mcpLaunch(pm: PackageManager): McpLaunch;
31
+ /** What writing one MCP config did. */
32
+ export type McpWriteStatus =
33
+ /** The file was created, or the server was added beside the ones in it. */
34
+ "added"
35
+ /** A `rebase` server was already there. It is left exactly as it was. */
36
+ | "present"
37
+ /** The file exists but is not something this can edit without losing data. */
38
+ | "unreadable";
39
+ export interface McpWriteResult {
40
+ /** Project-relative path of the config file. */
41
+ file: string;
42
+ status: McpWriteStatus;
43
+ }
44
+ /**
45
+ * The Codex block, as TOML. Appended rather than merged: there is no TOML
46
+ * parser here, and an append keeps every byte already in the file.
47
+ */
48
+ export declare function renderCodexServer(projectDir: string, launch: McpLaunch): string;
49
+ /**
50
+ * Register the Rebase MCP server in the agent's project-level config, started
51
+ * through the project's package manager (see {@link mcpLaunch}).
52
+ *
53
+ * Returns null for an agent with no project-level config to write.
54
+ */
55
+ export declare function writeMcpConfig(agentKey: AgentKey, projectDir: string, pm?: PackageManager): McpWriteResult | null;
56
+ /** What making sure the project depends on the MCP server did. */
57
+ export type McpDependencyStatus =
58
+ /** Added to `devDependencies`, at this CLI's version: an install is due. */
59
+ "added"
60
+ /** Already a dependency. Left at the version the project chose. */
61
+ | "present"
62
+ /** No `package.json`, or one that is not plain JSON: left alone. */
63
+ | "unreadable";
64
+ /**
65
+ * Make the project depend on {@link MCP_PACKAGE}, which the config written by
66
+ * {@link writeMcpConfig} runs from the project's own install.
67
+ *
68
+ * Pinned to this CLI's version, as `rebase init` pins every `@rebasepro`
69
+ * package; a project that already lists it keeps its own choice.
70
+ */
71
+ export declare function ensureMcpDependency(projectDir: string): McpDependencyStatus;
72
+ /**
73
+ * Point the scaffold's `.mcp.json` at the project's package manager.
74
+ *
75
+ * The template ships the pnpm spelling (`pnpm exec rebase-mcp`); an npm project
76
+ * has no pnpm to run it with. Only the entry the template wrote is rewritten.
77
+ */
78
+ export declare function retargetScaffoldMcpConfig(projectDir: string, pm: PackageManager): void;
79
+ /**
80
+ * Whether the agent's project-level config already registers the Rebase
81
+ * server: `null` for an agent that has no such config, `false` when the file
82
+ * is missing or does not name it. Reads only — for the hint `rebase skills
83
+ * install` prints when it was not asked to write one.
84
+ */
85
+ export declare function mcpServerRegistered(agentKey: AgentKey, projectDir: string): boolean | null;
86
+ /** What `configureAgents` did for one agent. */
87
+ export interface AgentSetupResult {
88
+ agent: AgentKey;
89
+ /** Skills written, or null when the bundle could not be loaded. */
90
+ skills: number | null;
91
+ /** An earlier agent in the same run that already wrote the skills directory. */
92
+ sharedWith?: AgentKey;
93
+ mcp: McpWriteResult | null;
94
+ }
95
+ /**
96
+ * Install the skills and register the MCP server for each agent.
97
+ *
98
+ * Never throws for a missing skills bundle — the scaffold it runs inside must
99
+ * not fail over its assistants — and reports it as `skills: null` instead.
100
+ */
101
+ export declare function configureAgents(agents: readonly AgentKey[], projectDir: string, load?: () => LoadedSkill[], pm?: PackageManager): {
102
+ results: AgentSetupResult[];
103
+ skillsError?: string;
104
+ mcpDependency?: McpDependencyStatus;
105
+ };
106
+ /** The line to print when {@link ensureMcpDependency} changed or could not change the manifest. */
107
+ export declare function mcpDependencyNote(status: McpDependencyStatus | undefined, pm: PackageManager): string | undefined;
108
+ /** One line per agent, for the scaffold's output. */
109
+ export declare function printAgentSetup({ results, skillsError, mcpDependency }: ReturnType<typeof configureAgents>, pm?: PackageManager): void;
110
+ /**
111
+ * Ask whether to set up AI agents, then which, pre-ticking what is installed.
112
+ *
113
+ * Interactive only; the caller has already refused a non-TTY. Returns [] when
114
+ * the developer says no or unticks everything.
115
+ */
116
+ export declare function promptForAgents(home?: string): Promise<AgentKey[]>;
@@ -1,40 +1,92 @@
1
+ import { type ApiKeyMasked, type ScopeSummary } from "@rebasepro/types";
1
2
  /** Everything the switch below dispatches, for the did-you-mean. */
2
- export declare const API_KEYS_SUBCOMMANDS: readonly ["list", "create", "revoke"];
3
+ export declare const API_KEYS_SUBCOMMANDS: readonly ["list", "get", "create", "revoke", "scopes"];
4
+ /**
5
+ * What to print when a request fails: the server's error code and message,
6
+ * which is what says why — `SCOPE_EXCEEDS_CREATOR`, `INVALID_SCOPES`,
7
+ * `ROLE_EXCEEDS_CREATOR`, `KEY_MANAGEMENT_SCOPE` — and a hint where one helps.
8
+ *
9
+ * Exported so its tests can read the lines without a server.
10
+ */
11
+ export declare function describeFailure(action: string, error: unknown, baseUrl: string): string[];
3
12
  export declare function apiKeysCommand(subcommand: string | undefined, rawArgs: string[]): Promise<void>;
13
+ /**
14
+ * The lines that describe one key: what it is, what it may do and whom it
15
+ * runs as.
16
+ *
17
+ * A service key runs as the RLS role `service` plus its own `roles`; a
18
+ * personal key runs as its owner's roles as they are when it is used, so it
19
+ * has none of its own to show.
20
+ *
21
+ * Exported so its tests can read the lines without a server.
22
+ */
23
+ export declare function describeKey(key: ApiKeyMasked, now?: Date): string[];
24
+ /** The flags `rebase api-keys list` takes: none of its own. */
25
+ export declare const LIST_KEYS_FLAGS: {};
26
+ /** The flags `rebase api-keys get` takes. */
27
+ export declare const GET_KEY_FLAGS: {
28
+ readonly "--id": StringConstructor;
29
+ };
30
+ /**
31
+ * Which key this invocation names, as a positional or as `--id`.
32
+ *
33
+ * Exported so its tests can drive the real parser rather than a copy of it.
34
+ */
35
+ export declare function resolveGetKeyArgs(rawArgs: string[]): {
36
+ id?: string;
37
+ };
4
38
  /** The flags `rebase api-keys create` takes. */
5
39
  export declare const CREATE_KEY_FLAGS: {
6
- readonly "--name": StringConstructor;
7
- readonly "--permissions": StringConstructor;
8
- readonly "--full-access": BooleanConstructor;
9
- readonly "--admin": BooleanConstructor;
10
- readonly "--rate-limit": NumberConstructor;
11
- readonly "--expires": StringConstructor;
12
- readonly "-n": "--name";
40
+ "--name": StringConstructor;
41
+ "--scopes": [StringConstructor];
42
+ "--full-access": BooleanConstructor;
43
+ "--roles": [StringConstructor];
44
+ "--rate-limit": NumberConstructor;
45
+ "--expires-in": StringConstructor;
46
+ "--expires-at": StringConstructor;
47
+ "-n": string;
13
48
  };
49
+ /**
50
+ * Options `create` refuses by name, each with the flag that does that job.
51
+ *
52
+ * Checked before the line is parsed: the strict parser would answer them
53
+ * "unknown option", which does not say what to type instead.
54
+ */
55
+ export declare const RETIRED_CREATE_OPTIONS: Readonly<Record<string, string>>;
56
+ /** What `rebase api-keys create` was asked to mint. */
57
+ export interface CreateKeyPlan {
58
+ name: string;
59
+ /** The scopes named with `--scopes`, or `"held"` for `--full-access`. */
60
+ scopes: string[] | "held";
61
+ /** RLS roles beside `service`. */
62
+ roles: string[];
63
+ rate_limit: number | null;
64
+ expires_at: string | null;
65
+ }
14
66
  /**
15
67
  * What this invocation asks to be created.
16
68
  *
17
69
  * The name may be given either way — `--name "My Key"` or as the single
18
70
  * positional — and under the old permissive parse an undeclared flag became
19
71
  * that positional: `rebase api-keys create --debug --full-access` created a
20
- * key called `--debug` with read/write/delete on every collection, and
21
- * `--debug` is what the CLI prints after every failure as the thing to re-run
22
- * with. Strict parsing makes the flag an error instead of a name.
72
+ * key called `--debug` with access to everything, and `--debug` is what the
73
+ * CLI prints after every failure as the thing to re-run with. Strict parsing
74
+ * makes the flag an error instead of a name.
75
+ *
76
+ * What the key may do has to be asked for by name — `--scopes` or
77
+ * `--full-access` — never defaulted: a forgotten flag must not mint a key that
78
+ * reaches everything.
23
79
  *
24
80
  * Exported so its tests can drive the real parser rather than a copy of it.
25
81
  */
26
- export declare function resolveCreateKeyArgs(rawArgs: string[]): {
27
- flags: import("arg").Result<{
28
- readonly "--name": StringConstructor;
29
- readonly "--permissions": StringConstructor;
30
- readonly "--full-access": BooleanConstructor;
31
- readonly "--admin": BooleanConstructor;
32
- readonly "--rate-limit": NumberConstructor;
33
- readonly "--expires": StringConstructor;
34
- readonly "-n": "--name";
35
- }>;
36
- name: string;
37
- };
82
+ export declare function resolveCreateKeyArgs(rawArgs: string[], now?: Date): CreateKeyPlan;
83
+ /**
84
+ * The scopes `--full-access` puts on a key: every one the caller holds, less
85
+ * key management, which no key may hold.
86
+ *
87
+ * Exported so its tests can check the subtraction without a server.
88
+ */
89
+ export declare function grantableScopes(held: readonly string[]): string[];
38
90
  /** The flags `rebase api-keys revoke` takes. */
39
91
  export declare const REVOKE_KEY_FLAGS: {
40
92
  readonly "--id": StringConstructor;
@@ -52,3 +104,12 @@ export declare const REVOKE_KEY_FLAGS: {
52
104
  export declare function resolveRevokeKeyArgs(rawArgs: string[]): {
53
105
  id?: string;
54
106
  };
107
+ /** The flags `rebase api-keys scopes` takes: none of its own. */
108
+ export declare const SCOPES_LIST_FLAGS: {};
109
+ /**
110
+ * The scope catalogue as printed lines: one section per plane, each scope with
111
+ * its label and what its target names, marked when the caller holds it.
112
+ *
113
+ * Exported so its tests can read the lines without a server.
114
+ */
115
+ export declare function describeScopes(scopes: readonly ScopeSummary[], held: readonly string[]): string[];
@@ -1,3 +1,20 @@
1
+ import type { RebaseAppConfig } from "@rebasepro/types";
2
+ import { loadManifest } from "../manifest.js";
1
3
  /** Everything the switch below dispatches, for the did-you-mean. */
2
4
  export declare const APPS_SUBCOMMANDS: readonly ["list", "init", "config"];
3
5
  export declare function appsCommand(subcommand: string | undefined, rawArgs?: string[]): Promise<void>;
6
+ /**
7
+ * One app's line in `rebase apps list`.
8
+ *
9
+ * A static app shows its full address — the https URL when its `path` gives it
10
+ * a hostname — and the CMS as the address a browser would open. `CMS at /` on
11
+ * an app that lives on `admin.example.com` reads as the project's own root,
12
+ * which is the one place that CMS is not.
13
+ */
14
+ export declare function describeApp(name: string, app: RebaseAppConfig): string;
15
+ /**
16
+ * Whether `rebase build` folds this static app into the backend bundle, which
17
+ * then serves it on the API's own origin: a managed backend, and an app with
18
+ * built output to fold. The same predicate the build folds by.
19
+ */
20
+ export declare function servedByBackend(manifest: ReturnType<typeof loadManifest>["manifest"], appName: string): boolean;
@@ -80,6 +80,13 @@ export declare function generatePassword(): string;
80
80
  * The script the direct-database fallback runs under the project's own tsx, so
81
81
  * it resolves the project's `@rebasepro/server-postgres` and schema.
82
82
  *
83
+ * It ends the account's sessions in the transaction that sets the password,
84
+ * as every door through the API does (`replaceUserPassword`): the refresh
85
+ * tokens and outstanding reset links go, and `tokens_valid_after` voids the
86
+ * access tokens already handed out. An operator resetting a phished account
87
+ * with the backend down is the case this exists for, and a reset that left the
88
+ * attacker's refresh token minting access tokens did not recover anything.
89
+ *
83
90
  * It exits 0 only when a row was updated: a missing user and a thrown error are
84
91
  * both exit 1. `echoPassword` prints the new password, for a generated one.
85
92
  *
@@ -39,7 +39,11 @@ export declare function buildCommand(rawArgs?: string[]): Promise<void>;
39
39
  * with its JSON error.
40
40
  */
41
41
  export declare function buildAssetApp(projectRoot: string, name: string, app: RebaseAppConfig, runtimeRange: string, outOverride?: string,
42
- /** `quietStdout`: every line, the build command's included, goes to stderr. See `toolStdio`. */
42
+ /**
43
+ * `quietStdout`: every line, the build command's included, goes to stderr. See `toolStdio`.
44
+ * `builtApps`: the static apps already built in this run (see `FoldOptions.builtApps`).
45
+ */
43
46
  options?: {
44
47
  quietStdout?: boolean;
48
+ builtApps?: Set<string>;
45
49
  }): Promise<string | undefined>;
@@ -20,9 +20,10 @@ export declare const LOGIN_FLAGS: {
20
20
  * neither is something this CLI can redact after the fact — the same reasoning
21
21
  * `rls-check` states for its connection string, which carries one too.
22
22
  *
23
- * There is no machine token yet, so CI genuinely does need a human's password;
24
- * `REBASE_CLOUD_PASSWORD` is how a secret store hands it over without it
25
- * appearing on a command line.
23
+ * CI should not need a password at all: `rebase cloud tokens create` mints a
24
+ * token narrowed to one project, and `REBASE_TOKEN` hands it to every cloud
25
+ * command. Where a login is still wanted, `REBASE_CLOUD_PASSWORD` is how a
26
+ * secret store hands the password over without it appearing on a command line.
26
27
  */
27
28
  export declare const PASSWORD_ENV = "REBASE_CLOUD_PASSWORD";
28
29
  export declare const EMAIL_ENV = "REBASE_CLOUD_EMAIL";
@@ -126,3 +126,48 @@ export declare function declaredAppsFrom(manifest: {
126
126
  } | null | undefined): DeclaredApp[];
127
127
  /** Upload a bundle archive; returns the control-plane bundle id. */
128
128
  export declare function uploadBundle(url: string, token: string, projectId: string, tarPath: string): Promise<string>;
129
+ /** One DNS record a hostname needs, as the control plane lists it. */
130
+ export interface DeployHostRecord {
131
+ type: string;
132
+ name: string;
133
+ values: string[];
134
+ }
135
+ /**
136
+ * A hostname an app in this deploy is declared on, as the deploy found it.
137
+ *
138
+ * The control plane registers each one on the project as it deploys — an app
139
+ * whose `path` is `https://admin.example.com` needs that host on the project
140
+ * before anything can route it — and a new one starts `pending`, because it is
141
+ * served only once its DNS is published and verified. `records` is what to
142
+ * publish, present for a pending host.
143
+ */
144
+ export interface DeployHost {
145
+ host: string;
146
+ status: "verified" | "pending";
147
+ records?: DeployHostRecord[];
148
+ }
149
+ /**
150
+ * The `hosts` a deploy response carries, read defensively.
151
+ *
152
+ * `undefined` when there is no such field, which is what a control plane that
153
+ * predates host-routed apps answers: the caller then leaves `hosts` out of its
154
+ * own output rather than reporting an empty set nobody looked for. An entry
155
+ * with no hostname is dropped, and any status but `verified` is pending — the
156
+ * same rule `cloud domains` reads rows by, because an unreadable status must
157
+ * not print as live.
158
+ */
159
+ export declare function deployHostsFrom(response: unknown): DeployHost[] | undefined;
160
+ /**
161
+ * What to tell somebody whose deploy declared a hostname that is not live yet.
162
+ *
163
+ * A deploy that succeeds with an app on a hostname nobody can reach reads as a
164
+ * broken deploy, so each pending host gets its own block: that it is registered
165
+ * but waiting, the records to create, and the command that finishes the job.
166
+ * The records come from the control plane, never composed here — A or CNAME
167
+ * depends on apex-vs-subdomain and on the ingress behind the project, which
168
+ * the CLI cannot know. Plain text: the caller colours it.
169
+ */
170
+ export declare function pendingHostNotices(hosts: DeployHost[]): Array<{
171
+ headline: string;
172
+ details: string[];
173
+ }>;
@@ -59,6 +59,24 @@ export type CloudClient = ReturnType<typeof createRebaseClient>;
59
59
  * by `requireClient`.
60
60
  */
61
61
  export declare function createCloudClient(url: string): CloudClient;
62
+ /**
63
+ * The environment variable that authenticates the cloud family with a token
64
+ * instead of a `cloud login` session — for CI and agents, which have no
65
+ * terminal to log in at and should not hold a person's password.
66
+ */
67
+ export declare const TOKEN_ENV = "REBASE_TOKEN";
68
+ /** `REBASE_TOKEN`, trimmed, or undefined when it is unset or blank. */
69
+ export declare function envToken(): string | undefined;
70
+ /**
71
+ * A client that sends `token` as its bearer and nothing else.
72
+ *
73
+ * No session: the token is not refreshed, and the credentials file is neither
74
+ * read nor written — a CI job's token must not become, or replace, the login
75
+ * of whoever owns the machine. A 401 is answered by {@link explainRefusedToken}
76
+ * rather than by the SDK's session refresh, because there is no session to
77
+ * refresh and "log in again" is the wrong remedy for a token.
78
+ */
79
+ export declare function createTokenClient(url: string, token: string): CloudClient;
62
80
  /** The half of a client that holds its session — all {@link freshAccessToken} reads. */
63
81
  export interface SessionHolder {
64
82
  auth: {
@@ -81,17 +99,36 @@ export interface SessionHolder {
81
99
  * token read once at the start of a command that runs for longer than it
82
100
  * lives — a deploy's build, a `db connect` left open — is refused by the time
83
101
  * it is used, so it is read here at the moment it is sent.
102
+ *
103
+ * A client built from `REBASE_TOKEN` has no session; its token is returned
104
+ * as it is, since there is nothing to refresh it with.
84
105
  */
85
106
  export declare function freshAccessToken(client: SessionHolder): Promise<string>;
86
107
  /**
87
- * Return an authenticated client for the resolved host, refreshing the access
88
- * token if it is close to expiry. Exits with a helpful message when there is no
89
- * usable session (never logged in, or the refresh token was revoked).
108
+ * Return an authenticated client for the resolved host.
109
+ *
110
+ * With `REBASE_TOKEN` set, that token is the credential for every request and
111
+ * the stored login is not consulted at all. Otherwise it is the `cloud login`
112
+ * session — see {@link requireSessionClient}.
90
113
  */
91
114
  export declare function requireClient(rawArgs: string[]): Promise<{
92
115
  client: CloudClient;
93
116
  url: string;
94
117
  }>;
118
+ /**
119
+ * Return a client holding the `cloud login` session for the resolved host,
120
+ * refreshing the access token if it is close to expiry, whether or not
121
+ * `REBASE_TOKEN` is set. Exits with a helpful message when there is no usable
122
+ * session (never logged in, or the refresh token was revoked).
123
+ *
124
+ * For the commands a token cannot run — minting and revoking tokens: a key that
125
+ * could mint keys could mint its own successor, so the control plane refuses
126
+ * every key on those routes.
127
+ */
128
+ export declare function requireSessionClient(rawArgs: string[]): Promise<{
129
+ client: CloudClient;
130
+ url: string;
131
+ }>;
95
132
  /** One place a deploy can actually land, as the control plane describes it. */
96
133
  export interface DeployTarget {
97
134
  clusterId?: string | null;
@@ -192,8 +229,10 @@ export declare function removeLink(cwd?: string): boolean;
192
229
  * positional.
193
230
  *
194
231
  * Only genuinely global flags belong here. Group-specific ones (`--bucket`,
195
- * `--region`, …) are declared by the handler that owns them and always follow
196
- * the group, so they cannot shift the group or action.
232
+ * `--region`, …) are declared by the handler that owns them. They can still
233
+ * precede the action — or stand in for it, on a group with a default — so the
234
+ * dispatcher also declares every one that takes a value: `CLOUD_VALUE_FLAGS`
235
+ * in `index.ts`.
197
236
  *
198
237
  * `-p` is `--project` in eighteen places and `--password` in `login`. That
199
238
  * ambiguity does not matter to the one caller that reads this spec: it resolves