@nestcorehq/cli 0.8.0 → 0.10.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 (3) hide show
  1. package/README.md +256 -214
  2. package/dist/index.js +355 -38
  3. package/package.json +42 -41
package/README.md CHANGED
@@ -1,214 +1,256 @@
1
- # @nestcorehq/cli
2
-
3
- The command-line interface for the [NestCore](https://nestcore.io) platform.
4
-
5
- `nestcore` brings the day-to-day backend workflow into your terminal and your
6
- repo: sign in once, link a directory to a project, run versioned SQL
7
- migrations, and generate TypeScript types straight from your live schema — the
8
- same ergonomics you expect from a modern backend toolchain.
9
-
10
- ```bash
11
- npm install -g @nestcorehq/cli
12
- # or run without installing:
13
- npx @nestcorehq/cli --help
14
- ```
15
-
16
- > Requires Node.js 22+.
17
-
18
- ---
19
-
20
- ## Quick start
21
-
22
- ```bash
23
- # 1. Sign in (credentials are stored in ~/.nestcore/credentials.json, mode 0600)
24
- nestcore login
25
-
26
- # 2. Link the current directory to one of your projects
27
- nestcore link my-project # by slug, name, or id
28
- # → writes .nestcore/config.json (safe to commit)
29
-
30
- # 3. Scaffold and apply a migration
31
- nestcore migration new "create posts table"
32
- # → edits migrations/001_create_posts_table.sql
33
- nestcore db push
34
-
35
- # 4. Generate types from the live schema
36
- nestcore gen types # → nestcore.types.ts
37
- ```
38
-
39
- ---
40
-
41
- ## Commands
42
-
43
- ### Auth
44
-
45
- | Command | Description |
46
- | --- | --- |
47
- | `nestcore login` | Sign in. Reads `--email` / `--password`, then `NESTCORE_EMAIL` / `NESTCORE_PASSWORD`, then prompts interactively (password input is masked). |
48
- | `nestcore logout` | Revoke the refresh token and clear the local session. |
49
- | `nestcore whoami` | Show the current session, API URL, and token expiry. |
50
-
51
- ### Projects
52
-
53
- | Command | Description |
54
- | --- | --- |
55
- | `nestcore projects` | List your projects (name, slug, id). |
56
- | `nestcore link [id\|slug\|name]` | Link the current directory to a project. With no argument and a single project, links it automatically; otherwise lists choices. Writes `.nestcore/config.json`. |
57
-
58
- ### Database & migrations
59
-
60
- | Command | Description |
61
- | --- | --- |
62
- | `nestcore migration new <name>` | Scaffold `migrations/NNN_<name>.sql` with `up` / `-- migrate:down` sections. |
63
- | `nestcore db push` | Apply every pending migration in order, inside a transaction each. |
64
- | `nestcore db status` | Show which migrations are applied, pending, failed, or modified-after-apply. |
65
- | `nestcore db rollback [id]` | Roll back the latest applied migration (or a specific `id`) using its `-- migrate:down` section. |
66
- | `nestcore db reset --yes` | **Destructive.** Drop every table in the project (internal `auth_*` tables are spared) and re-apply all migrations from scratch. Requires `--yes`. First dry-runs the drop + every migration in a rolled-back transaction; if that fails, nothing is dropped. |
67
- | `nestcore db sql "<SQL>"` | Run ad-hoc SQL against the linked project. `--file <path>` runs a file. |
68
- | `nestcore db connect` | Print the direct Postgres connection string (when the deployment has direct connections enabled). |
69
- | `nestcore gen types` | Introspect the schema and emit TypeScript interfaces + a `Database` map (`bigint`/`numeric` and enums are typed `string`, `json`/`jsonb` `unknown` — matching what the API returns). `--out <file>` (default `nestcore.types.ts`) or `--stdout`. |
70
-
71
- ### Integrations
72
-
73
- | Command | Description |
74
- | --- | --- |
75
- | `nestcore webhooks list` | List the project's webhooks. |
76
- | `nestcore webhooks create --url <u> [--events INSERT,UPDATE,DELETE] [--table <t>] [--name <n>]` | Create a webhook (prints the signing secret once). |
77
- | `nestcore webhooks delete <id>` / `test <id>` | Remove a webhook / send it a test event. |
78
- | `nestcore sms get` | Show the project's Twilio/SMS config. |
79
- | `nestcore sms set [--sid <s>] [--token <t>] [--from <n>] [--enable\|--disable]` | Configure Twilio (auth token stored encrypted). |
80
- | `nestcore sms test <phone>` | Send a test SMS. |
81
-
82
- ### AI
83
-
84
- | Command | Description |
85
- | --- | --- |
86
- | `nestcore ai overview` | Counts of memories, memory subjects, collections, documents, threads, messages, traces (last 24 h) and embedded columns. Shows zeros until the project uses an AI feature. |
87
- | `nestcore ai embeddings list` | Embedded columns with progress: rows, embedded, pending, failed and the last error. |
88
- | `nestcore ai embeddings enable --table <t> --columns <a,b> [--target <col>] [--model <m>] [--provider <p>]` | Keep a `vector(768)` column (default `<first column>_embedding`) up to date from the source columns, and backfill existing rows. Admin role. |
89
- | `nestcore ai embeddings reembed <table> <column> [--model <m>] [--provider <p>]` | Re-embed every row, optionally switching model. Admin role. |
90
- | `nestcore ai embeddings disable <table> <column> [--drop-columns]` | Stop embedding a column; `--drop-columns` also drops the vector columns. Admin role. |
91
- | `nestcore ai collections list` | Vector collections with their model, document counts and chunking. |
92
- | `nestcore ai traces [--limit n] [--status ok\|error]` | Recent AI calls (default 20, max 500): kind, caller, model, tokens, cost, latency and status. |
93
-
94
- ### Options
95
-
96
- | Option | Applies to | Description |
97
- | --- | --- | --- |
98
- | `-p, --project <id\|slug>` | db / gen / webhooks / sms / auth-security / ai | Target a specific project, overriding the linked one. |
99
- | `-d, --dir <path>` | migration / db | Migrations directory (default `migrations`). |
100
- | `-o, --out <file>` | gen types | Output file. |
101
- | `--stdout` | gen types | Print to stdout instead of a file. |
102
- | `-f, --file <path>` | db sql | Read SQL from a file. |
103
- | `--url <url>` | login | API base URL (default `https://api.nestcore.io`). |
104
- | `-h, --help` | — | Show help. |
105
- | `-v, --version` | — | Show version. |
106
-
107
- ---
108
-
109
- ## Migrations
110
-
111
- A migration is a plain `.sql` file named `NNN_description.sql`. The numeric
112
- prefix defines apply order. An optional `-- migrate:down` line splits the file
113
- into a forward (`up`) section and a rollback (`down`) section:
114
-
115
- ```sql
116
- -- Migration 001: create posts table
117
- -- Forward (up) SQL — applied by `nestcore db push`.
118
- CREATE TABLE posts (
119
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
120
- title text NOT NULL,
121
- created_at timestamptz DEFAULT now()
122
- );
123
-
124
- -- migrate:down
125
- -- Rollback SQL — applied by `nestcore db rollback`.
126
- DROP TABLE posts;
127
- ```
128
-
129
- How it works:
130
-
131
- - A `migrations` registry table tracks `migration_id`, `name`, `checksum`,
132
- `executed_at`, and `status`.
133
- - Each migration runs wrapped in `BEGIN; … COMMIT;` — a failure rolls the whole
134
- file back and is recorded as `failed`.
135
- - Already-applied migrations are skipped. If an applied file's checksum no
136
- longer matches, `db push` refuses to continue (add a new migration instead of
137
- editing history).
138
-
139
- ---
140
-
141
- ## Type generation
142
-
143
- `nestcore gen types` reads `information_schema.columns` for your `public`
144
- schema (skipping internal `auth_*`, `pg_*`, and `_*` tables) and emits a
145
- strongly-typed module:
146
-
147
- ```ts
148
- // Generated by @nestcorehq/cli — do not edit by hand.
149
- export interface Posts {
150
- id: string;
151
- title: string;
152
- created_at?: string | null;
153
- }
154
-
155
- export interface Database {
156
- "posts": Posts;
157
- }
158
- ```
159
-
160
- Re-run it after every schema change and commit the output, or wire it into a
161
- `predev` / `prebuild` script.
162
-
163
- ---
164
-
165
- ## Permissions
166
-
167
- Raw SQL (`db sql`, `db push`/`status`/`rollback`/`reset`, `gen types`, `db diff`) runs only for the user who **created** the project; other workspace members get a 403 explaining this.
168
-
169
- ## Configuration & files
170
-
171
- | Path | Scope | Contents |
172
- | --- | --- | --- |
173
- | `~/.nestcore/credentials.json` | global, `0600` | Access/refresh tokens, API URL, user. Shared with the NestCore MCP server; token refreshes are serialised through `~/.nestcore/credentials.lock`. |
174
- | `./.nestcore/config.json` | per-directory | Linked `projectId`, `projectSlug`, `name`, `apiUrl` (same file the MCP server reads; older files with `slug` still work). Safe to commit. |
175
-
176
- Environment variables (override stored values):
177
-
178
- - `NESTCORE_API_URL` — API base URL.
179
- - `NESTCORE_EMAIL`, `NESTCORE_PASSWORD` — non-interactive login.
180
- - `NESTCORE_MFA_CODE` — two-factor code for non-interactive login.
181
- - `NO_COLOR` — disable ANSI colors.
182
-
183
- ### Upgrading from `@orbitneststudio/cli`
184
-
185
- The CLI was renamed from OrbitNest to NestCore. Existing setups keep working:
186
-
187
- - `orbitnest` is installed as a second command name for `nestcore`.
188
- - `~/.orbitnest/credentials.json` is read when `~/.nestcore/credentials.json`
189
- doesn't exist yet; new sessions are written to `~/.nestcore` (and mirrored to
190
- the old file while it exists, so older tools sharing it stay signed in).
191
- - `./.orbitnest/config.json` and `./.orbitnest/schema.snapshot.json` are read
192
- when the `.nestcore` equivalents are absent; `link` and `db diff --save` write
193
- the new paths.
194
- - The old `ORBITNEST_*` environment variables are used when the `NESTCORE_*`
195
- ones are unset.
196
-
197
- ---
198
-
199
- ## Development
200
-
201
- ```bash
202
- npm install
203
- npm run build # bundle to dist/ with tsup (CJS + shebang)
204
- npm run dev # watch mode
205
- npm run typecheck # tsc --noEmit
206
-
207
- node dist/index.js --help
208
- ```
209
-
210
- ---
211
-
212
- ## License
213
-
214
- MIT © NestCore
1
+ # @nestcorehq/cli
2
+
3
+ The command-line interface for the [NestCore](https://nestcore.io) platform.
4
+
5
+ `nestcore` brings the day-to-day backend workflow into your terminal and your
6
+ repo: sign in once, link a directory to a project, run versioned SQL
7
+ migrations, and generate TypeScript types straight from your live schema — the
8
+ same ergonomics you expect from a modern backend toolchain.
9
+
10
+ ```bash
11
+ npm install -g @nestcorehq/cli
12
+ # or run without installing:
13
+ npx @nestcorehq/cli --help
14
+ ```
15
+
16
+ > Requires Node.js 22+.
17
+
18
+ ---
19
+
20
+ ## Quick start
21
+
22
+ ```bash
23
+ # 1. Sign in (credentials are stored in ~/.nestcore/credentials.json, mode 0600)
24
+ nestcore login
25
+
26
+ # 2. Link the current directory to one of your projects
27
+ nestcore link my-project # by slug, name, or id
28
+ # → writes .nestcore/config.json (safe to commit)
29
+
30
+ # 3. Scaffold and apply a migration
31
+ nestcore migration new "create posts table"
32
+ # → edits migrations/001_create_posts_table.sql
33
+ nestcore db push
34
+
35
+ # 4. Generate types from the live schema
36
+ nestcore gen types # → nestcore.types.ts
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Commands
42
+
43
+ ### Auth
44
+
45
+ | Command | Description |
46
+ | --- | --- |
47
+ | `nestcore login` | Sign in. Reads `--email` / `--password`, then `NESTCORE_EMAIL` / `NESTCORE_PASSWORD`, then prompts interactively (password input is masked). |
48
+ | `nestcore logout` | Revoke the refresh token and clear the local session. |
49
+ | `nestcore whoami` | Show the current session, API URL, and token expiry. |
50
+
51
+ ### Projects
52
+
53
+ | Command | Description |
54
+ | --- | --- |
55
+ | `nestcore projects` | List your projects (name, slug, id). |
56
+ | `nestcore link [id\|slug\|name]` | Link the current directory to a project. With no argument and a single project, links it automatically; otherwise lists choices. Writes `.nestcore/config.json`. Add `--branch development` to make Development the default for this directory. |
57
+
58
+ ### Branches
59
+
60
+ Every App project has a **Production** branch (the live project) and a **Development** branch: a copy of Production's schema and config (tables, RLS, functions, buckets, auth settings) with its own data, end users, URL and keys. Schema changes you make on Development are recorded and can be promoted to Production.
61
+
62
+ | Command | Description |
63
+ | --- | --- |
64
+ | `nestcore branches` | Production and Development: status, database and file sizes, changes waiting to be promoted. |
65
+ | `nestcore branches create` | Create the Development branch (schema + config, no data). |
66
+ | `nestcore branches reset --yes` | Rebuild Development from Production. Deletes its rows, end users and files; keeps its URL, keys, env vars and functions. |
67
+ | `nestcore branches changes` | Schema changes recorded on Development and not promoted yet (`!` = can lose data). |
68
+ | `nestcore branches discard <id…>` | Drop recorded changes so they are never promoted. |
69
+ | `nestcore branches promote` | Preview what would be promoted. |
70
+ | `nestcore branches promote --yes` | Promote (workspace admins). Schema changes run in one transaction on Production; then functions, new buckets and new jobs (created off). Add `--env` for missing env vars, `--auth-settings` to copy auth settings, `--confirm-destructive` for DROP / RENAME / type changes, `--acknowledge-drift` if Production changed meanwhile. |
71
+ | `nestcore branches delete --yes` | Delete the Development branch (workspace admins). |
72
+
73
+ Target a branch on any command with `--branch production` or `--branch development` (`-b`, also `prod` / `dev`), or link the directory with `--branch`. A typical flow:
74
+
75
+ ```bash
76
+ nestcore link my-app --branch development
77
+ nestcore db push # migrations go to Development
78
+ nestcore gen types
79
+ nestcore branches promote # review
80
+ nestcore branches promote --yes # apply to Production
81
+ ```
82
+
83
+ ### Database & migrations
84
+
85
+ | Command | Description |
86
+ | --- | --- |
87
+ | `nestcore migration new <name>` | Scaffold `migrations/NNN_<name>.sql` with `up` / `-- migrate:down` sections. |
88
+ | `nestcore db push` | Apply every pending migration in order, inside a transaction each. |
89
+ | `nestcore db status` | Show which migrations are applied, pending, failed, or modified-after-apply. |
90
+ | `nestcore db rollback [id]` | Roll back the latest applied migration (or a specific `id`) using its `-- migrate:down` section. |
91
+ | `nestcore db reset --yes` | **Destructive.** Drop every table in the project (internal `auth_*` tables are spared) and re-apply all migrations from scratch. Requires `--yes`. First dry-runs the drop + every migration in a rolled-back transaction; if that fails, nothing is dropped. |
92
+ | `nestcore db sql "<SQL>"` | Run ad-hoc SQL against the linked project. `--file <path>` runs a file. |
93
+ | `nestcore db connect` | Print the direct Postgres connection string (when the deployment has direct connections enabled). |
94
+ | `nestcore gen types` | Introspect the schema and emit TypeScript interfaces + a `Database` map (`bigint`/`numeric` and enums are typed `string`, `json`/`jsonb` `unknown` — matching what the API returns). `--out <file>` (default `nestcore.types.ts`) or `--stdout`. |
95
+
96
+ ### Integrations
97
+
98
+ | Command | Description |
99
+ | --- | --- |
100
+ | `nestcore webhooks list` | List the project's webhooks. |
101
+ | `nestcore webhooks create --url <u> [--events INSERT,UPDATE,DELETE] [--table <t>] [--name <n>]` | Create a webhook (prints the signing secret once). |
102
+ | `nestcore webhooks delete <id>` / `test <id>` | Remove a webhook / send it a test event. |
103
+ | `nestcore sms get` | Show the project's Twilio/SMS config. |
104
+ | `nestcore sms set [--sid <s>] [--token <t>] [--from <n>] [--enable\|--disable]` | Configure Twilio (auth token stored encrypted). |
105
+ | `nestcore sms test <phone>` | Send a test SMS. |
106
+
107
+ ### AI
108
+
109
+ | Command | Description |
110
+ | --- | --- |
111
+ | `nestcore ai overview` | Counts of memories, memory subjects, collections, documents, threads, messages, traces (last 24 h) and embedded columns. Shows zeros until the project uses an AI feature. |
112
+ | `nestcore ai embeddings list` | Embedded columns with progress: rows, embedded, pending, failed and the last error. |
113
+ | `nestcore ai embeddings enable --table <t> --columns <a,b> [--target <col>] [--model <m>] [--provider <p>]` | Keep a `vector(768)` column (default `<first column>_embedding`) up to date from the source columns, and backfill existing rows. Admin role. |
114
+ | `nestcore ai embeddings reembed <table> <column> [--model <m>] [--provider <p>]` | Re-embed every row, optionally switching model. Admin role. |
115
+ | `nestcore ai embeddings disable <table> <column> [--drop-columns]` | Stop embedding a column; `--drop-columns` also drops the vector columns. Admin role. |
116
+ | `nestcore ai collections list` | Vector collections with their model, document counts and chunking. |
117
+ | `nestcore ai traces [--limit n] [--status ok\|error]` | Recent AI calls (default 20, max 500): kind, caller, model, tokens, cost, latency and status. |
118
+
119
+ ### AI credit
120
+
121
+ Prepaid AI credit belongs to the workspace and is shared by all its projects. It pays for AI calls of projects
122
+ without their own provider key (a project's own key always wins), at the platform's key plus the margin. A
123
+ workspace that never bought credit is never charged.
124
+
125
+ | Command | Description |
126
+ | --- | --- |
127
+ | `nestcore credits` | Balance, monthly cap, this month's spend, whether credit can pay for a call now, the credit packs and the recent ledger. |
128
+ | `nestcore credits buy <pack> [--open]` | Print a Stripe Checkout link for a pack (e.g. `credits_10`) to open and pay in the browser; `--open` opens it. Nothing is charged until you pay, and the credit appears once Stripe confirms. Admin role. |
129
+ | `nestcore credits cap <usd\|off>` | Set the monthly cap on credit spend (e.g. `20` or `12.50`), or remove it with `off`. Admin role. |
130
+
131
+ An AI call paid from credit fails with `CREDITS_EXHAUSTED` when the balance is gone or the cap is reached; the CLI
132
+ prints which one, the next step and the buy link.
133
+
134
+ ### Options
135
+
136
+ | Option | Applies to | Description |
137
+ | --- | --- | --- |
138
+ | `-p, --project <id\|slug>` | db / gen / webhooks / sms / auth-security / ai / branches | Target a specific project, overriding the linked one. |
139
+ | `-b, --branch <name>` | db / gen / webhooks / sms / auth-security / ai | `production` or `development` (`prod` / `dev`), overriding the linked branch. |
140
+ | `-d, --dir <path>` | migration / db | Migrations directory (default `migrations`). |
141
+ | `-o, --out <file>` | gen types | Output file. |
142
+ | `--stdout` | gen types | Print to stdout instead of a file. |
143
+ | `-f, --file <path>` | db sql | Read SQL from a file. |
144
+ | `--url <url>` | login | API base URL (default `https://api.nestcore.io`). |
145
+ | `-h, --help` | — | Show help. |
146
+ | `-v, --version` | — | Show version. |
147
+
148
+ ---
149
+
150
+ ## Migrations
151
+
152
+ A migration is a plain `.sql` file named `NNN_description.sql`. The numeric
153
+ prefix defines apply order. An optional `-- migrate:down` line splits the file
154
+ into a forward (`up`) section and a rollback (`down`) section:
155
+
156
+ ```sql
157
+ -- Migration 001: create posts table
158
+ -- Forward (up) SQL — applied by `nestcore db push`.
159
+ CREATE TABLE posts (
160
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
161
+ title text NOT NULL,
162
+ created_at timestamptz DEFAULT now()
163
+ );
164
+
165
+ -- migrate:down
166
+ -- Rollback SQL — applied by `nestcore db rollback`.
167
+ DROP TABLE posts;
168
+ ```
169
+
170
+ How it works:
171
+
172
+ - A `migrations` registry table tracks `migration_id`, `name`, `checksum`,
173
+ `executed_at`, and `status`.
174
+ - Each migration runs wrapped in `BEGIN; … COMMIT;` — a failure rolls the whole
175
+ file back and is recorded as `failed`.
176
+ - Already-applied migrations are skipped. If an applied file's checksum no
177
+ longer matches, `db push` refuses to continue (add a new migration instead of
178
+ editing history).
179
+
180
+ ---
181
+
182
+ ## Type generation
183
+
184
+ `nestcore gen types` reads `information_schema.columns` for your `public`
185
+ schema (skipping internal `auth_*`, `pg_*`, and `_*` tables) and emits a
186
+ strongly-typed module:
187
+
188
+ ```ts
189
+ // Generated by @nestcorehq/cli — do not edit by hand.
190
+ export interface Posts {
191
+ id: string;
192
+ title: string;
193
+ created_at?: string | null;
194
+ }
195
+
196
+ export interface Database {
197
+ "posts": Posts;
198
+ }
199
+ ```
200
+
201
+ Re-run it after every schema change and commit the output, or wire it into a
202
+ `predev` / `prebuild` script.
203
+
204
+ ---
205
+
206
+ ## Permissions
207
+
208
+ Raw SQL (`db sql`, `db push`/`status`/`rollback`/`reset`, `gen types`, `db diff`) runs only for the user who **created** the project; other workspace members get a 403 explaining this.
209
+
210
+ ## Configuration & files
211
+
212
+ | Path | Scope | Contents |
213
+ | --- | --- | --- |
214
+ | `~/.nestcore/credentials.json` | global, `0600` | Access/refresh tokens, API URL, user. Shared with the NestCore MCP server; token refreshes are serialised through `~/.nestcore/credentials.lock`. |
215
+ | `./.nestcore/config.json` | per-directory | Linked `projectId`, `projectSlug`, `name`, `apiUrl` and optional `branch` (same file the MCP server reads; older files with `slug` still work). Safe to commit. |
216
+
217
+ Environment variables (override stored values):
218
+
219
+ - `NESTCORE_API_URL` — API base URL.
220
+ - `NESTCORE_EMAIL`, `NESTCORE_PASSWORD` — non-interactive login.
221
+ - `NESTCORE_MFA_CODE` — two-factor code for non-interactive login.
222
+ - `NO_COLOR` — disable ANSI colors.
223
+
224
+ ### Upgrading from `@orbitneststudio/cli`
225
+
226
+ The CLI was renamed from OrbitNest to NestCore. Existing setups keep working:
227
+
228
+ - `orbitnest` is installed as a second command name for `nestcore`.
229
+ - `~/.orbitnest/credentials.json` is read when `~/.nestcore/credentials.json`
230
+ doesn't exist yet; new sessions are written to `~/.nestcore` (and mirrored to
231
+ the old file while it exists, so older tools sharing it stay signed in).
232
+ - `./.orbitnest/config.json` and `./.orbitnest/schema.snapshot.json` are read
233
+ when the `.nestcore` equivalents are absent; `link` and `db diff --save` write
234
+ the new paths.
235
+ - The old `ORBITNEST_*` environment variables are used when the `NESTCORE_*`
236
+ ones are unset.
237
+
238
+ ---
239
+
240
+ ## Development
241
+
242
+ ```bash
243
+ npm install
244
+ npm run build # bundle to dist/ with tsup (CJS + shebang)
245
+ npm run dev # watch mode
246
+ npm run typecheck # tsc --noEmit
247
+ npm test # build, then run test/*.mjs against a local fake API
248
+
249
+ node dist/index.js --help
250
+ ```
251
+
252
+ ---
253
+
254
+ ## License
255
+
256
+ MIT © NestCore
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/index.ts
27
+ var import_child_process = require("child_process");
27
28
  var import_fs4 = require("fs");
28
29
  var readline = __toESM(require("readline"));
29
30
 
@@ -83,6 +84,13 @@ var CRED_LOCK = (0, import_path.join)(CRED_DIR, "credentials.lock");
83
84
  var LINK_DIR = (0, import_path.resolve)(".nestcore");
84
85
  var LINK_FILE = (0, import_path.join)(LINK_DIR, "config.json");
85
86
  var LEGACY_LINK_FILE = (0, import_path.resolve)(".orbitnest", "config.json");
87
+ function parseBranch(value) {
88
+ if (value === void 0) return void 0;
89
+ const v = value.toLowerCase();
90
+ if (v === "production" || v === "prod" || v === "main") return "production";
91
+ if (v === "development" || v === "dev") return "development";
92
+ fail(`Unknown branch "${value}". Use production or development.`);
93
+ }
86
94
  function loadCredentials() {
87
95
  for (const file of [CRED_FILE, LEGACY_CRED_FILE]) {
88
96
  if (!(0, import_fs.existsSync)(file)) continue;
@@ -154,7 +162,7 @@ function loadLink() {
154
162
  const file = (0, import_fs.existsSync)(LINK_FILE) || !(0, import_fs.existsSync)(LEGACY_LINK_FILE) ? LINK_FILE : LEGACY_LINK_FILE;
155
163
  const raw = JSON.parse((0, import_fs.readFileSync)(file, "utf8"));
156
164
  if (!raw?.projectId) return null;
157
- return { projectId: raw.projectId, projectSlug: raw.projectSlug ?? raw.slug, name: raw.name, apiUrl: raw.apiUrl };
165
+ return { projectId: raw.projectId, projectSlug: raw.projectSlug ?? raw.slug, name: raw.name, apiUrl: raw.apiUrl, branch: raw.branch };
158
166
  } catch {
159
167
  return null;
160
168
  }
@@ -183,11 +191,21 @@ function jwtExpiry(token) {
183
191
  }
184
192
  }
185
193
  var ApiError = class extends Error {
186
- constructor(message, status2) {
194
+ /**
195
+ * `code` is the platform's own error code when it sent one (QUOTA_EXCEEDED,
196
+ * CREDITS_EXHAUSTED, AGENT_PROJECT_RESTRICTED, NOT_AN_AGENT_PROJECT, PROJECT_PAUSED, …);
197
+ * `body` keeps structured fields such as QUOTA_EXCEEDED's upgradeUrl or
198
+ * CREDITS_EXHAUSTED's reason and buyUrl.
199
+ */
200
+ constructor(message, status2, code, body) {
187
201
  super(message);
188
202
  this.status = status2;
203
+ this.code = code;
204
+ this.body = body;
189
205
  }
190
206
  status;
207
+ code;
208
+ body;
191
209
  };
192
210
  var Api = class {
193
211
  constructor(base, token, refresher) {
@@ -243,7 +261,20 @@ var Api = class {
243
261
  if (res.status === 403 && /restricted to the project owner/i.test(String(msg))) {
244
262
  msg += ". Raw SQL (incl. migrations, db reset, gen types, db diff), dropping tables, RLS toggles and policy changes are limited to the user who created the project. Ask the project owner to run this.";
245
263
  }
246
- throw new ApiError(msg, res.status);
264
+ const code = typeof json?.code === "string" ? json.code : void 0;
265
+ if (code === "QUOTA_EXCEEDED" && json?.upgradeUrl) msg += `
266
+ Upgrade: ${json.upgradeUrl}`;
267
+ if (code === "PROJECT_PAUSED") msg += "\n Resume it with `nestcore projects resume <id|slug>` or upgrade your plan.";
268
+ if (code === "DESTRUCTIVE_CHANGES") msg += "\n Re-run with --confirm-destructive, or drop those changes with `nestcore branches discard <id>`.";
269
+ if (code === "PRODUCTION_DRIFT") msg += "\n Review `nestcore branches promote`, then re-run with --acknowledge-drift.";
270
+ if (code === "PROMOTE_FAILED" && json?.failed_change?.statement) msg += `
271
+ Failed statement: ${json.failed_change.statement}`;
272
+ if (code === "CREDITS_EXHAUSTED") {
273
+ msg += json?.reason === "spend_cap" ? "\n The workspace's monthly AI credit cap is reached. Raise or remove it with `nestcore credits cap <usd|off>`, add the project's own provider key, or wait for next month." : "\n The workspace has no AI credit left. Buy a pack with `nestcore credits buy <pack>` or add the project's own provider key.";
274
+ if (json?.buyUrl) msg += `
275
+ Buy credit: ${json.buyUrl}`;
276
+ }
277
+ throw new ApiError(msg, res.status, code, json ?? void 0);
247
278
  }
248
279
  return json;
249
280
  }
@@ -297,17 +328,37 @@ function matchProject(projects, ref) {
297
328
  if (!byName[0]) fail(`No project matching "${ref}". Run \`nestcore projects\` to see them.`);
298
329
  return byName[0];
299
330
  }
300
- async function resolveProjectId(api, flagProject) {
331
+ async function resolveProjectId(api, flagProject, flagBranch) {
332
+ const link = loadLink();
333
+ const branch = parseBranch(flagBranch) ?? (flagProject ? void 0 : link?.branch) ?? "production";
334
+ let base;
301
335
  if (flagProject) {
302
- if (UUID_RE.test(flagProject)) return { id: flagProject };
303
- const res = await api.get("/projects");
304
- const projects = Array.isArray(res) ? res : res?.data ?? res?.projects ?? [];
305
- const match = matchProject(projects, flagProject);
306
- return { id: match.id, slug: match.slug };
336
+ if (UUID_RE.test(flagProject)) base = { id: flagProject };
337
+ else {
338
+ const res = await api.get("/projects");
339
+ const projects = Array.isArray(res) ? res : res?.data ?? res?.projects ?? [];
340
+ const match = matchProject(projects, flagProject);
341
+ base = { id: match.id, slug: match.slug };
342
+ }
343
+ } else if (link) {
344
+ base = { id: link.projectId, slug: link.projectSlug };
345
+ } else {
346
+ fail("No project selected. Run `nestcore link <project>` or pass `--project <id|slug>`.");
307
347
  }
308
- const link = loadLink();
309
- if (link) return { id: link.projectId, slug: link.projectSlug };
310
- fail("No project selected. Run `nestcore link <project>` or pass `--project <id|slug>`.");
348
+ return { ...await resolveBranch(api, base.id, branch, base.slug), branch };
349
+ }
350
+ async function resolveBranch(api, projectId, branch, slug) {
351
+ const res = await api.get(`/projects/${projectId}/branches`).catch(() => null);
352
+ const list = res?.data ?? res;
353
+ const rows = list?.branches ?? [];
354
+ if (!rows.length) {
355
+ if (branch === "development") fail("Branches are not available for this project (Agent project or older API).");
356
+ return { id: projectId, slug };
357
+ }
358
+ const row = rows.find((b) => b.branch === branch);
359
+ if (!row) fail("This project has no Development branch yet. Run `nestcore branches create`.");
360
+ if (row.provisioning_status !== "ready") fail(`The ${branch} branch is ${row.provisioning_status}. Try again in a moment.`);
361
+ return { id: row.id, slug: row.slug };
311
362
  }
312
363
 
313
364
  // src/migrate.ts
@@ -653,7 +704,7 @@ function describeColumn(c) {
653
704
  // package.json
654
705
  var package_default = {
655
706
  name: "@nestcorehq/cli",
656
- version: "0.8.0",
707
+ version: "0.10.0",
657
708
  description: "Command-line interface for the NestCore platform \u2014 auth, projects, migrations, type generation, and SQL.",
658
709
  bin: {
659
710
  nestcore: "dist/index.js",
@@ -673,6 +724,7 @@ var package_default = {
673
724
  build: "tsup",
674
725
  dev: "tsup --watch",
675
726
  typecheck: "tsc --noEmit",
727
+ test: "npm run build && node test/credits.mjs",
676
728
  start: "node dist/index.js"
677
729
  },
678
730
  keywords: [
@@ -702,7 +754,8 @@ var ALIAS = {
702
754
  d: "dir",
703
755
  h: "help",
704
756
  v: "version",
705
- e: "email"
757
+ e: "email",
758
+ b: "branch"
706
759
  };
707
760
  function parseArgs(argv) {
708
761
  const positionals = [];
@@ -854,22 +907,51 @@ async function cmdWhoami() {
854
907
  warn(`Session check failed (${e.message}). Try \`nestcore login\`.`);
855
908
  }
856
909
  }
857
- async function cmdProjects() {
858
- const projects = listFrom(await authed().get("/projects"));
910
+ var PROJECT_KINDS = ["app", "agent"];
911
+ async function cmdProjects(rest, flags) {
912
+ const sub = rest[0];
913
+ if (sub === "eligibility") {
914
+ const kind2 = rest[1] || str(flags, "kind") || "app";
915
+ if (!PROJECT_KINDS.includes(kind2)) fail("Usage: nestcore projects eligibility [app|agent]");
916
+ const e = await authed().get("/projects/eligibility", { kind: kind2 });
917
+ if (e.allowed) success(`You can create ${kind2 === "agent" ? "an agent" : "an app"} project (${e.plan} plan, ${e.used}/${e.limit ?? "\u221E"}).`);
918
+ else warn(`No room for ${kind2 === "agent" ? "an agent" : "an app"} project on the ${e.plan} plan (${e.used}/${e.limit}). Upgrade: ${e.upgradeUrl}`);
919
+ return;
920
+ }
921
+ if (sub === "pause" || sub === "resume") {
922
+ const ref = rest[1];
923
+ if (!ref) fail(`Usage: nestcore projects ${sub} <id|slug>`);
924
+ const match = matchProject(listFrom(await authed().get("/projects")), ref);
925
+ await authed().post(`/projects/${encodeURIComponent(match.id)}/status`, { status: sub === "pause" ? "paused" : "active" });
926
+ success(`${sub === "pause" ? "Paused" : "Resumed"} ${color.bold(match.name)}.`);
927
+ return;
928
+ }
929
+ if (sub && sub !== "list") fail("Usage: nestcore projects [list|eligibility|pause|resume] [--kind app|agent]");
930
+ const kind = str(flags, "kind") || void 0;
931
+ if (kind && !PROJECT_KINDS.includes(kind)) fail("--kind must be app or agent");
932
+ const projects = listFrom(await authed().get("/projects", { kind }));
859
933
  heading(`Projects (${projects.length})`);
860
934
  table(
861
- projects.map((p) => ({ name: String(p.name ?? ""), slug: String(p.slug ?? ""), id: String(p.id ?? "") })),
862
- ["name", "slug", "id"]
935
+ projects.map((p) => ({
936
+ name: String(p.name ?? ""),
937
+ slug: String(p.slug ?? ""),
938
+ kind: String(p.kind ?? "app"),
939
+ status: String(p.status ?? "active"),
940
+ id: String(p.id ?? "")
941
+ })),
942
+ ["name", "slug", "kind", "status", "id"]
863
943
  );
864
944
  }
865
- async function cmdLink(positionals) {
945
+ async function cmdLink(positionals, flags) {
866
946
  const ref = positionals[0];
947
+ const branch = parseBranch(str(flags, "branch"));
867
948
  const projects = listFrom(await authed().get("/projects"));
949
+ const branchNote = branch === "development" ? ` on the ${color.bold("development")} branch` : "";
868
950
  if (!ref) {
869
951
  if (projects.length === 1) {
870
952
  const p = projects[0];
871
- saveLink({ projectId: p.id, projectSlug: p.slug, name: p.name, apiUrl: apiUrl() });
872
- success(`Linked to ${color.bold(p.name)} (${p.slug}).`);
953
+ saveLink({ projectId: p.id, projectSlug: p.slug, name: p.name, apiUrl: apiUrl(), ...branch ? { branch } : {} });
954
+ success(`Linked to ${color.bold(p.name)} (${p.slug})${branchNote}.`);
873
955
  return;
874
956
  }
875
957
  heading("Specify a project to link: `nestcore link <id|slug>`");
@@ -877,8 +959,9 @@ async function cmdLink(positionals) {
877
959
  return;
878
960
  }
879
961
  const match = matchProject(projects, ref);
880
- saveLink({ projectId: match.id, projectSlug: match.slug, name: match.name, apiUrl: apiUrl() });
881
- success(`Linked this directory to ${color.bold(match.name)} (${match.slug}).`);
962
+ saveLink({ projectId: match.id, projectSlug: match.slug, name: match.name, apiUrl: apiUrl(), ...branch ? { branch } : {} });
963
+ success(`Linked this directory to ${color.bold(match.name)} (${match.slug})${branchNote}.`);
964
+ if (branch === "development") log(color.dim(" Commands now target Development; pass --branch production to reach the live project."));
882
965
  log(color.dim(" Wrote .nestcore/config.json"));
883
966
  }
884
967
  function cmdMigrationNew(positionals, flags) {
@@ -890,7 +973,7 @@ function cmdMigrationNew(positionals, flags) {
890
973
  }
891
974
  async function cmdDbPush(flags) {
892
975
  const api = authed();
893
- const { id } = await resolveProjectId(api, str(flags, "project"));
976
+ const { id } = await resolveProjectId(api, str(flags, "project"), str(flags, "branch"));
894
977
  const dir = str(flags, "dir") || "migrations";
895
978
  const sql = sqlRunner(api, id);
896
979
  const res = await push(sql, dir);
@@ -899,7 +982,7 @@ async function cmdDbPush(flags) {
899
982
  }
900
983
  async function cmdDbStatus(flags) {
901
984
  const api = authed();
902
- const { id } = await resolveProjectId(api, str(flags, "project"));
985
+ const { id } = await resolveProjectId(api, str(flags, "project"), str(flags, "branch"));
903
986
  const dir = str(flags, "dir") || "migrations";
904
987
  const entries = await status(sqlRunner(api, id), dir);
905
988
  heading(`Migrations (${entries.filter((e) => e.applied).length}/${entries.length} applied)`);
@@ -914,14 +997,14 @@ async function cmdDbStatus(flags) {
914
997
  }
915
998
  async function cmdDbRollback(positionals, flags) {
916
999
  const api = authed();
917
- const { id } = await resolveProjectId(api, str(flags, "project"));
1000
+ const { id } = await resolveProjectId(api, str(flags, "project"), str(flags, "branch"));
918
1001
  const dir = str(flags, "dir") || "migrations";
919
1002
  const rolled = await rollback(sqlRunner(api, id), dir, positionals[0]);
920
1003
  success(`Rolled back migration ${color.bold(rolled)}.`);
921
1004
  }
922
1005
  async function cmdDbReset(flags) {
923
1006
  const api = authed();
924
- const { id } = await resolveProjectId(api, str(flags, "project"));
1007
+ const { id } = await resolveProjectId(api, str(flags, "project"), str(flags, "branch"));
925
1008
  const dir = str(flags, "dir") || "migrations";
926
1009
  if (!flags.yes && !flags.force) {
927
1010
  fail("`db reset` DROPS every table in your project and re-runs all migrations. Re-run with --yes to confirm.");
@@ -935,7 +1018,7 @@ async function cmdDbReset(flags) {
935
1018
  }
936
1019
  async function cmdDbSql(positionals, flags) {
937
1020
  const api = authed();
938
- const { id } = await resolveProjectId(api, str(flags, "project"));
1021
+ const { id } = await resolveProjectId(api, str(flags, "project"), str(flags, "branch"));
939
1022
  const file = str(flags, "file");
940
1023
  const sqlText = file ? (0, import_fs4.readFileSync)(file, "utf8") : positionals.join(" ");
941
1024
  if (!sqlText.trim()) fail('Usage: nestcore db sql "<SQL>" (or --file <path>)');
@@ -946,7 +1029,7 @@ ${rows.length} row(s)`));
946
1029
  }
947
1030
  async function cmdGenTypes(flags) {
948
1031
  const api = authed();
949
- const { id } = await resolveProjectId(api, str(flags, "project"));
1032
+ const { id } = await resolveProjectId(api, str(flags, "project"), str(flags, "branch"));
950
1033
  const ts = await generateTypes(sqlRunner(api, id));
951
1034
  if (flags.stdout) {
952
1035
  process.stdout.write(ts);
@@ -958,7 +1041,7 @@ async function cmdGenTypes(flags) {
958
1041
  }
959
1042
  async function cmdDbDiff(flags) {
960
1043
  const api = authed();
961
- const { id } = await resolveProjectId(api, str(flags, "project"));
1044
+ const { id } = await resolveProjectId(api, str(flags, "project"), str(flags, "branch"));
962
1045
  const current = await introspectSchema(sqlRunner(api, id));
963
1046
  if (flags.save) {
964
1047
  const path = saveSnapshot(current);
@@ -992,7 +1075,7 @@ async function cmdDbDiff(flags) {
992
1075
  }
993
1076
  async function cmdDbConnect(flags) {
994
1077
  const api = authed();
995
- const { id } = await resolveProjectId(api, str(flags, "project"));
1078
+ const { id } = await resolveProjectId(api, str(flags, "project"), str(flags, "branch"));
996
1079
  let res;
997
1080
  try {
998
1081
  res = await api.get(`/admin/projects/${encodeURIComponent(id)}/connection`);
@@ -1024,7 +1107,7 @@ async function cmdDbConnect(flags) {
1024
1107
  }
1025
1108
  async function cmdWebhooks(rest, flags) {
1026
1109
  const api = authed();
1027
- const { id } = await resolveProjectId(api, str(flags, "project"));
1110
+ const { id } = await resolveProjectId(api, str(flags, "project"), str(flags, "branch"));
1028
1111
  const base = `/admin/projects/${encodeURIComponent(id)}/webhooks`;
1029
1112
  const sub = rest[0] ?? "list";
1030
1113
  if (sub === "list") {
@@ -1072,7 +1155,7 @@ var AUTH_SECURITY_FLAGS = [
1072
1155
  ];
1073
1156
  async function cmdAuthSecurity(rest, flags) {
1074
1157
  const api = authed();
1075
- const { id } = await resolveProjectId(api, str(flags, "project"));
1158
+ const { id } = await resolveProjectId(api, str(flags, "project"), str(flags, "branch"));
1076
1159
  const base = `/projects/${encodeURIComponent(id)}/auth-settings`;
1077
1160
  const sub = rest[0] ?? "get";
1078
1161
  const show = (limits) => {
@@ -1100,7 +1183,7 @@ async function cmdAuthSecurity(rest, flags) {
1100
1183
  }
1101
1184
  async function cmdSms(rest, flags) {
1102
1185
  const api = authed();
1103
- const { id } = await resolveProjectId(api, str(flags, "project"));
1186
+ const { id } = await resolveProjectId(api, str(flags, "project"), str(flags, "branch"));
1104
1187
  const base = `/admin/projects/${encodeURIComponent(id)}/sms-config`;
1105
1188
  const sub = rest[0] ?? "get";
1106
1189
  if (sub === "get") {
@@ -1156,7 +1239,7 @@ function showEmbeddings(cols) {
1156
1239
  }
1157
1240
  async function cmdAi(rest, flags) {
1158
1241
  const api = authed();
1159
- const { id } = await resolveProjectId(api, str(flags, "project"));
1242
+ const { id } = await resolveProjectId(api, str(flags, "project"), str(flags, "branch"));
1160
1243
  const pid = encodeURIComponent(id);
1161
1244
  const area = rest[0] ?? "overview";
1162
1245
  if (area === "overview") {
@@ -1259,6 +1342,103 @@ Traces are kept ${d.retentionDays} day(s) on this plan. Prompts and replies are
1259
1342
  }
1260
1343
  fail(AI_USAGE);
1261
1344
  }
1345
+ var CREDITS_USAGE = "Usage: nestcore credits [show] | credits buy <pack> [--open] | credits cap <usd|off>";
1346
+ var money = (n) => {
1347
+ if (n === null || n === void 0) return "\u2014";
1348
+ const v = Number(n);
1349
+ const abs = Math.abs(v);
1350
+ const s = abs === 0 || abs >= 0.01 ? `$${abs.toFixed(2)}` : usd(abs);
1351
+ return v < 0 ? `-${s}` : s;
1352
+ };
1353
+ function openInBrowser(url) {
1354
+ if (!/^https:\/\//i.test(url)) return false;
1355
+ const [cmd, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
1356
+ try {
1357
+ const child = (0, import_child_process.spawn)(cmd, args, { stdio: "ignore", detached: true });
1358
+ child.on("error", () => {
1359
+ });
1360
+ child.unref();
1361
+ return true;
1362
+ } catch {
1363
+ return false;
1364
+ }
1365
+ }
1366
+ function showCreditSummary(c) {
1367
+ heading("AI credit (workspace)");
1368
+ table(
1369
+ [
1370
+ { item: "balance", value: money(c.balanceUsd) },
1371
+ { item: "monthly cap", value: c.monthlyCapUsd === null || c.monthlyCapUsd === void 0 ? "none" : money(c.monthlyCapUsd) },
1372
+ { item: "spent this month", value: money(c.spentThisMonthUsd) },
1373
+ { item: "available now", value: c.available ? color.green("yes") : color.yellow("no") },
1374
+ { item: "margin", value: `${c.marginPct ?? 0}%` }
1375
+ ],
1376
+ ["item", "value"]
1377
+ );
1378
+ if (!c.active) {
1379
+ info("This workspace never bought credit, so it is never charged. Projects without their own provider key can't use the AI gateway until it does.");
1380
+ } else if (!c.available) {
1381
+ warn(c.balanceUsd <= 0 ? "No credit left: buy a pack with `nestcore credits buy <pack>`." : "The monthly cap is reached: raise or remove it with `nestcore credits cap <usd|off>`.");
1382
+ }
1383
+ }
1384
+ async function cmdCredits(rest, flags) {
1385
+ const api = authed();
1386
+ const sub = rest[0] ?? "show";
1387
+ if (sub === "show") {
1388
+ const c = await api.get("/billing/credits");
1389
+ showCreditSummary(c);
1390
+ heading("Packs");
1391
+ table(
1392
+ (c.packs ?? []).map((p) => ({ pack: String(p.id), price: money(p.priceUsd), credit: money(p.creditsUsd) })),
1393
+ ["pack", "price", "credit"]
1394
+ );
1395
+ const ledger = Array.isArray(c.ledger) ? c.ledger : [];
1396
+ heading(`Recent activity (${ledger.length})`);
1397
+ table(
1398
+ ledger.map((l) => ({
1399
+ time: shortTime(l.created_at),
1400
+ kind: String(l.kind ?? ""),
1401
+ amount: money(l.amount_usd),
1402
+ description: String(l.description ?? "")
1403
+ })),
1404
+ ["time", "kind", "amount", "description"]
1405
+ );
1406
+ log(color.dim("\nCredit pays for AI calls of projects without their own provider key; a project's own key always wins."));
1407
+ return;
1408
+ }
1409
+ if (sub === "buy") {
1410
+ const pack = rest[1];
1411
+ if (!pack) {
1412
+ const c = await api.get("/billing/credits");
1413
+ fail(`${CREDITS_USAGE}
1414
+ Packs: ${(c.packs ?? []).map((p) => `${p.id} (${money(p.priceUsd)} \u2192 ${money(p.creditsUsd)} credit)`).join(", ")}`);
1415
+ }
1416
+ if (!/^[a-z0-9_-]{1,40}$/.test(pack)) fail(`"${pack}" is not a pack id. Run \`nestcore credits\` to see the packs.`);
1417
+ const res = await api.post("/billing/credits/checkout", { pack, ref: "cli" });
1418
+ if (!res?.url) fail("The API returned no checkout URL.");
1419
+ info(`Checkout for ${color.bold(pack)}${res.pack?.priceUsd ? ` (${money(res.pack.priceUsd)})` : ""}. Open this link to pay with Stripe:`);
1420
+ log(` ${res.url}`);
1421
+ if (flags.open === true && openInBrowser(res.url)) info("Opened it in your browser.");
1422
+ log(color.dim("Nothing is charged until you pay. The credit shows up in `nestcore credits` once Stripe confirms the payment."));
1423
+ return;
1424
+ }
1425
+ if (sub === "cap") {
1426
+ const v = rest[1];
1427
+ if (!v) fail(CREDITS_USAGE);
1428
+ let cap;
1429
+ if (["off", "none", "null"].includes(v.toLowerCase())) cap = null;
1430
+ else {
1431
+ if (!/^\d+(\.\d{1,2})?$/.test(v)) fail('The cap is a USD amount like 20 or 12.50, or "off" to remove it.');
1432
+ cap = Number(v);
1433
+ if (cap > 1e5) fail("The cap can be at most 100000 USD.");
1434
+ }
1435
+ const c = await api.put("/billing/credits/cap", { monthlyCapUsd: cap });
1436
+ success(cap === null ? "Monthly AI credit cap removed." : `Monthly AI credit cap set to ${money(cap)}.`);
1437
+ showCreditSummary(c);
1438
+ return;
1439
+ }
1440
+ fail(CREDITS_USAGE);
1441
+ }
1262
1442
  function printHelp() {
1263
1443
  log(`${color.bold("nestcore")} ${color.dim("v" + VERSION)} \u2014 NestCore platform CLI
1264
1444
 
@@ -1271,8 +1451,25 @@ ${color.bold("Auth")}
1271
1451
  whoami Show the current session
1272
1452
 
1273
1453
  ${color.bold("Projects")}
1274
- projects List your projects
1454
+ projects List your projects with kind and status (--kind app|agent)
1455
+ projects eligibility [app|agent]
1456
+ Can you create one more project of a kind? (shows the upgrade link)
1457
+ projects pause|resume <id|slug>
1458
+ Pause a project (nothing is deleted) or resume it
1275
1459
  link <id|slug> Link the current directory to a project (.nestcore/config.json)
1460
+ --branch development: work on the Development branch by default
1461
+
1462
+ ${color.bold("Branches (Production + Development)")}
1463
+ branches Production and Development: status, sizes, changes to promote
1464
+ branches create Create the Development branch (schema + config, no data)
1465
+ branches reset --yes Rebuild Development from Production (deletes its data, users, files)
1466
+ branches changes Schema changes recorded on Development, not yet promoted
1467
+ branches discard <id\u2026> Drop recorded changes so they are never promoted
1468
+ branches promote Preview what would be promoted to Production
1469
+ branches promote --yes [--confirm-destructive] [--acknowledge-drift] [--env] [--auth-settings]
1470
+ Apply it (admin): schema in one transaction, then functions,
1471
+ buckets, new jobs (off); --env adds missing env vars
1472
+ branches delete --yes Delete the Development branch (admin)
1276
1473
 
1277
1474
  ${color.bold("Database & migrations")}
1278
1475
  migration new <name> Scaffold a new migration file (NNN_name.sql)
@@ -1308,8 +1505,14 @@ ${color.bold("AI")}
1308
1505
  ai traces [--limit n] [--status ok|error]
1309
1506
  Recent model calls: tokens, cost, latency
1310
1507
 
1508
+ ${color.bold("AI credit (workspace)")}
1509
+ credits Balance, monthly cap, this month's spend, packs, recent activity
1510
+ credits buy <pack> [--open] Stripe Checkout link for a credit pack (--open: open it in the browser)
1511
+ credits cap <usd|off> Set or remove the monthly cap on credit spend
1512
+
1311
1513
  ${color.bold("Options")}
1312
1514
  -p, --project <id> Target project (overrides the linked one)
1515
+ -b, --branch <name> production | development (overrides the linked branch)
1313
1516
  -d, --dir <path> Migrations directory (default: migrations)
1314
1517
  -o, --out <file> Output file for gen types
1315
1518
  --url <url> API base URL for login (default: https://api.nestcore.io)
@@ -1320,6 +1523,115 @@ ${color.bold("Options")}
1320
1523
  ${color.dim("Migrations run DDL via the admin SQL endpoint. Files may include a")}
1321
1524
  ${color.dim("`-- migrate:down` section for rollback.")}`);
1322
1525
  }
1526
+ var human = (n) => {
1527
+ const v = Number(n);
1528
+ if (n == null || !Number.isFinite(v)) return "\u2014";
1529
+ if (v < 1024 * 1024) return `${Math.round(v / 1024)} KB`;
1530
+ return `${(v / 1024 / 1024).toFixed(1)} MB`;
1531
+ };
1532
+ async function cmdBranches(rest, flags) {
1533
+ const api = authed();
1534
+ const { id } = await resolveProjectId(api, str(flags, "project"), "production");
1535
+ const base = `/projects/${id}/branches`;
1536
+ const data = (r) => r?.data ?? r;
1537
+ const sub = rest[0] ?? "list";
1538
+ if (sub === "list" || sub === "ls") {
1539
+ const list = data(await api.get(base));
1540
+ if (!list?.branches?.length) fail("Branches are not available for this project.");
1541
+ table(
1542
+ list.branches.map((b) => ({
1543
+ branch: b.branch === "development" ? color.yellow("development") : color.green("production"),
1544
+ slug: String(b.slug),
1545
+ status: String(b.provisioning_status),
1546
+ database: human(b.database_bytes),
1547
+ files: human(b.storage_bytes),
1548
+ "to promote": b.branch === "development" ? String(b.pending_changes) : "",
1549
+ id: String(b.id)
1550
+ })),
1551
+ ["branch", "slug", "status", "database", "files", "to promote", "id"]
1552
+ );
1553
+ if (!list.branches.some((b) => b.branch === "development")) info("No Development branch yet: `nestcore branches create`.");
1554
+ return;
1555
+ }
1556
+ if (sub === "create") {
1557
+ info("Creating the Development branch (copies schema and config, not data)\u2026");
1558
+ const b = data(await api.post(`${base}/development`, {}));
1559
+ success(`Development branch ready: ${color.bold(b.slug)}`);
1560
+ log(color.dim(` URL: ${b.project_url}`));
1561
+ log(color.dim(" Use it: `nestcore link <project> --branch development`, or pass --branch development."));
1562
+ return;
1563
+ }
1564
+ if (sub === "reset") {
1565
+ if (!flags.yes) fail("Reset deletes every row, end user and file on Development and rebuilds it from Production. Re-run with --yes.");
1566
+ info("Rebuilding Development from Production\u2026");
1567
+ const b = data(await api.post(`${base}/development/reset`, { confirm: true }));
1568
+ success(`Development reset (${b.slug}).`);
1569
+ return;
1570
+ }
1571
+ if (sub === "delete") {
1572
+ if (!flags.yes) fail("Deleting removes the Development database, users, files and keys. Re-run with --yes.");
1573
+ await api.del(`${base}/development`);
1574
+ success("Development branch deleted.");
1575
+ return;
1576
+ }
1577
+ if (sub === "changes") {
1578
+ const changes = data(await api.get(`${base}/development/changes`));
1579
+ if (!changes.length) {
1580
+ info("No schema changes waiting to be promoted.");
1581
+ return;
1582
+ }
1583
+ for (const c of changes) {
1584
+ log(`${color.dim(String(c.seq).padStart(3))} ${c.destructive ? color.red("!") : " "} ${c.statement}`);
1585
+ log(color.dim(` ${c.source} \xB7 ${c.id}`));
1586
+ }
1587
+ return;
1588
+ }
1589
+ if (sub === "discard") {
1590
+ const ids = rest.slice(1);
1591
+ if (!ids.length) fail("Usage: nestcore branches discard <change-id\u2026>");
1592
+ const r = data(await api.post(`${base}/development/changes/discard`, { ids }));
1593
+ success(`Discarded ${r.discarded} change(s).`);
1594
+ return;
1595
+ }
1596
+ if (sub === "promote") {
1597
+ const p = data(await api.get(`${base}/development/promote`));
1598
+ heading(`Promote Development \u2192 Production (${p.production_name})`);
1599
+ if (p.drift?.changed) warn("Production's schema changed since Development was created, reset or last promoted.");
1600
+ if (p.changes.length) {
1601
+ log(color.bold("Schema changes (one transaction):"));
1602
+ for (const c of p.changes) log(` ${c.destructive ? color.red("! ") : " "}${c.statement}`);
1603
+ }
1604
+ const cfg = p.config;
1605
+ if (cfg.functions.length) log(`${color.bold("Functions:")} ${cfg.functions.map((f) => `${f.name} (${f.status})`).join(", ")}`);
1606
+ if (cfg.buckets.length) log(`${color.bold("Buckets:")} ${cfg.buckets.map((b) => b.name).join(", ")}`);
1607
+ const newJobs = cfg.jobs.filter((j) => j.status === "new");
1608
+ if (cfg.jobs.length) log(`${color.bold("Jobs:")} ${cfg.jobs.map((j) => `${j.name} (${j.status === "new" ? "new, created off" : "changed, update by hand"})`).join(", ")}`);
1609
+ if (cfg.env_vars.length) log(`${color.bold("Env vars missing on Production:")} ${cfg.env_vars.map((e) => e.name).join(", ")} ${flags.env ? "" : color.dim("(not included; add --env)")}`);
1610
+ if (cfg.auth_settings.changed) log(`${color.bold("Auth settings differ")} ${flags["auth-settings"] ? "" : color.dim("(not included; add --auth-settings)")}`);
1611
+ const empty = !p.changes.length && !cfg.functions.length && !cfg.buckets.length && !newJobs.length && !(flags.env && cfg.env_vars.length) && !(flags["auth-settings"] && cfg.auth_settings.changed);
1612
+ if (empty) {
1613
+ info("Nothing to promote.");
1614
+ return;
1615
+ }
1616
+ if (!flags.yes) {
1617
+ info("Preview only. Re-run with --yes to apply.");
1618
+ return;
1619
+ }
1620
+ const res = data(await api.post(`${base}/development/promote`, {
1621
+ change_ids: p.changes.map((c) => c.id),
1622
+ functions: cfg.functions.map((f) => f.name),
1623
+ buckets: cfg.buckets.map((b) => b.name),
1624
+ jobs: newJobs.map((j) => j.name),
1625
+ env_vars: flags.env ? cfg.env_vars.map((e) => e.name) : [],
1626
+ auth_settings: !!flags["auth-settings"],
1627
+ confirm_destructive: !!flags["confirm-destructive"],
1628
+ acknowledge_drift: !!flags["acknowledge-drift"]
1629
+ }));
1630
+ success(`Promoted to Production: ${JSON.stringify(res.summary)}`);
1631
+ return;
1632
+ }
1633
+ fail("Usage: nestcore branches [list|create|reset|changes|discard|promote|delete]");
1634
+ }
1323
1635
  async function main() {
1324
1636
  const { positionals, flags } = parseArgs(process.argv.slice(2));
1325
1637
  if (flags.version) {
@@ -1340,9 +1652,12 @@ async function main() {
1340
1652
  case "whoami":
1341
1653
  return cmdWhoami();
1342
1654
  case "projects":
1343
- return cmdProjects();
1655
+ return cmdProjects(rest, flags);
1344
1656
  case "link":
1345
- return cmdLink(rest);
1657
+ return cmdLink(rest, flags);
1658
+ case "branches":
1659
+ case "branch":
1660
+ return cmdBranches(rest, flags);
1346
1661
  case "migration":
1347
1662
  if (rest[0] === "new") return cmdMigrationNew(rest.slice(1), flags);
1348
1663
  return fail("Usage: nestcore migration new <name>");
@@ -1373,6 +1688,8 @@ async function main() {
1373
1688
  return cmdAuthSecurity(rest, flags);
1374
1689
  case "ai":
1375
1690
  return cmdAi(rest, flags);
1691
+ case "credits":
1692
+ return cmdCredits(rest, flags);
1376
1693
  case "gen":
1377
1694
  if (rest[0] === "types") return cmdGenTypes(flags);
1378
1695
  return fail("Usage: nestcore gen types");
package/package.json CHANGED
@@ -1,41 +1,42 @@
1
- {
2
- "name": "@nestcorehq/cli",
3
- "version": "0.8.0",
4
- "description": "Command-line interface for the NestCore platform — auth, projects, migrations, type generation, and SQL.",
5
- "bin": {
6
- "nestcore": "dist/index.js",
7
- "orbitnest": "dist/index.js"
8
- },
9
- "files": [
10
- "dist",
11
- "README.md"
12
- ],
13
- "publishConfig": {
14
- "access": "public"
15
- },
16
- "engines": {
17
- "node": ">=22.0.0"
18
- },
19
- "scripts": {
20
- "build": "tsup",
21
- "dev": "tsup --watch",
22
- "typecheck": "tsc --noEmit",
23
- "start": "node dist/index.js"
24
- },
25
- "keywords": [
26
- "nestcore",
27
- "cli",
28
- "backend-as-a-service",
29
- "migrations"
30
- ],
31
- "repository": {
32
- "type": "git",
33
- "url": "git+https://github.com/Orbitnest/nestcore_cli.git"
34
- },
35
- "license": "MIT",
36
- "devDependencies": {
37
- "@types/node": "^22.20.5",
38
- "tsup": "^8.0.0",
39
- "typescript": "^5.4.0"
40
- }
41
- }
1
+ {
2
+ "name": "@nestcorehq/cli",
3
+ "version": "0.10.0",
4
+ "description": "Command-line interface for the NestCore platform — auth, projects, migrations, type generation, and SQL.",
5
+ "bin": {
6
+ "nestcore": "dist/index.js",
7
+ "orbitnest": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "engines": {
17
+ "node": ">=22.0.0"
18
+ },
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "dev": "tsup --watch",
22
+ "typecheck": "tsc --noEmit",
23
+ "test": "npm run build && node test/credits.mjs",
24
+ "start": "node dist/index.js"
25
+ },
26
+ "keywords": [
27
+ "nestcore",
28
+ "cli",
29
+ "backend-as-a-service",
30
+ "migrations"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/Orbitnest/nestcore_cli.git"
35
+ },
36
+ "license": "MIT",
37
+ "devDependencies": {
38
+ "@types/node": "^22.20.5",
39
+ "tsup": "^8.0.0",
40
+ "typescript": "^5.4.0"
41
+ }
42
+ }