@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.
- package/README.md +256 -214
- package/dist/index.js +355 -38
- 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
|
-
###
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
|
63
|
-
|
|
|
64
|
-
| `nestcore
|
|
65
|
-
| `nestcore
|
|
66
|
-
| `nestcore
|
|
67
|
-
| `nestcore
|
|
68
|
-
| `nestcore
|
|
69
|
-
| `nestcore
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
| `nestcore
|
|
88
|
-
| `nestcore
|
|
89
|
-
| `nestcore
|
|
90
|
-
| `nestcore
|
|
91
|
-
| `nestcore
|
|
92
|
-
| `nestcore
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
--
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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.
|
|
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
|
-
|
|
858
|
-
|
|
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) => ({
|
|
862
|
-
|
|
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.
|
|
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
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
"
|
|
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
|
+
}
|