@norbix.ai/cli 0.1.0 → 0.3.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 +148 -13
- package/dist/base.js +92 -23
- package/dist/commands/api.js +15 -81
- package/dist/commands/configure.js +68 -0
- package/dist/commands/env/use.js +36 -7
- package/dist/commands/files/delete.js +1 -1
- package/dist/commands/files/download.js +1 -1
- package/dist/commands/files/info.js +1 -1
- package/dist/commands/files/integrations/test.js +85 -0
- package/dist/commands/files/list.js +1 -1
- package/dist/commands/files/publish.js +38 -0
- package/dist/commands/files/sign.js +1 -1
- package/dist/commands/files/unpublish.js +34 -0
- package/dist/commands/files/upload.js +1 -1
- package/dist/commands/hub.js +24 -0
- package/dist/commands/login.js +82 -36
- package/dist/commands/logout.js +11 -13
- package/dist/commands/profiles.js +25 -0
- package/dist/commands/push/archive.js +4 -1
- package/dist/commands/push/clone.js +4 -1
- package/dist/commands/push/delete.js +4 -1
- package/dist/commands/push/device/register.js +35 -0
- package/dist/commands/push/integration/default.js +19 -0
- package/dist/commands/push/integration/delete.js +28 -0
- package/dist/commands/push/integration/disable.js +19 -0
- package/dist/commands/push/integration/enable.js +19 -0
- package/dist/commands/push/integration/test.js +30 -0
- package/dist/commands/push/integration.js +16 -0
- package/dist/commands/push/integrations.js +20 -0
- package/dist/commands/push/preview.js +19 -0
- package/dist/commands/push/settings.js +16 -0
- package/dist/commands/push/unarchive.js +4 -1
- package/dist/commands/raw.js +86 -0
- package/dist/commands/whoami.js +25 -8
- package/dist/help.js +18 -0
- package/dist/lib/device-login.js +76 -0
- package/dist/lib/dispatch.js +151 -0
- package/dist/lib/filesIntegrationTest.js +57 -0
- package/dist/lib/namespace-command.js +148 -0
- package/dist/lib/profiles.js +138 -0
- package/dist/lib/publicFiles.js +63 -0
- package/oclif.manifest.json +3272 -737
- package/package.json +30 -13
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Manage your Norbix projects from the terminal. Built on top of the official
|
|
|
4
4
|
[`@norbix.ai/ts`](https://www.npmjs.com/package/@norbix.ai/ts) SDK, so every
|
|
5
5
|
command talks to the same API the SDK does.
|
|
6
6
|
|
|
7
|
-
Works on **macOS**, **Linux** and **Windows** (Node.js
|
|
7
|
+
Works on **macOS**, **Linux** and **Windows** (Node.js 22+).
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -39,7 +39,8 @@ norbix scheduler list
|
|
|
39
39
|
| `norbix login` / `logout` / `whoami` | Authenticate and inspect the current context |
|
|
40
40
|
| `norbix config list/get/set/unset` | Manage the local config file |
|
|
41
41
|
| `norbix db find/get/count/insert/update/delete/aggregate` | Work with database collections |
|
|
42
|
-
| `norbix files list/info/upload/download/sign/delete` | Upload, download and
|
|
42
|
+
| `norbix files list/info/upload/download/sign/delete/publish/unpublish` | Upload, download, manage and publish files |
|
|
43
|
+
| `norbix files integrations test <id>` | Check that a files integration really works (live upload/read/list/delete probe) |
|
|
43
44
|
| `norbix users list/get/invite/block/unblock/delete` | Manage project users (membership) |
|
|
44
45
|
| `norbix env list/use/delete` | Manage project environments |
|
|
45
46
|
| `norbix logs list/trail` | Read project logs, follow one correlation ID |
|
|
@@ -52,25 +53,62 @@ norbix scheduler list
|
|
|
52
53
|
| `norbix payments integrations/triggers/trigger/enable/disable` | Payment integrations and triggers |
|
|
53
54
|
| `norbix integrations <module>` | List integrations of any module |
|
|
54
55
|
| `norbix module enable/disable <name>` | Turn a whole project module on or off |
|
|
55
|
-
| `norbix
|
|
56
|
+
| `norbix hub <module> <words...>` | Call ANY hub endpoint with plain words (see below) |
|
|
57
|
+
| `norbix api <module> <words...>` | Same for the data-plane API |
|
|
58
|
+
| `norbix raw <path>` | Low-level HTTP escape hatch (hub by default, `--api` for API) |
|
|
59
|
+
|
|
60
|
+
### Plain-word access to every endpoint
|
|
61
|
+
|
|
62
|
+
`norbix hub` and `norbix api` resolve SDK methods from plain words — every
|
|
63
|
+
current and future SDK method is callable without waiting for a dedicated
|
|
64
|
+
command:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
norbix hub # list modules
|
|
68
|
+
norbix hub database # list database methods
|
|
69
|
+
norbix hub database aggregates get # plural = list
|
|
70
|
+
norbix hub database aggregate get maggr_123 # singular = one item
|
|
71
|
+
norbix hub database aggregates delete maggr_123 --schemaId sch_456
|
|
72
|
+
norbix hub scheduler tasks get --pageSize 100
|
|
73
|
+
norbix hub email templates get # email/sms/push route into notifications
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The first positional value becomes `id`; `--field value` flags become request
|
|
77
|
+
fields. Destructive verbs (delete, remove, rotate, ...) ask for confirmation
|
|
78
|
+
unless `--yes`. Add `--dry-run` to preview the exact SDK call.
|
|
56
79
|
| `norbix autocomplete` | Set up shell tab-completion (bash/zsh) |
|
|
57
80
|
|
|
58
81
|
Run `norbix <topic> --help` for flags and examples.
|
|
59
82
|
|
|
60
|
-
## Configuration
|
|
83
|
+
## Configuration: profiles + sessions
|
|
84
|
+
|
|
85
|
+
Two ways to authenticate, and they work together:
|
|
86
|
+
|
|
87
|
+
**Profiles (AWS-style)** — one INI file at `~/.norbix/config`, set up with:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
norbix configure # writes the [default] profile
|
|
91
|
+
norbix configure --profile fitskin # a named profile
|
|
92
|
+
norbix db find orders --profile fitskin
|
|
93
|
+
```
|
|
61
94
|
|
|
62
|
-
|
|
95
|
+
It asks for a service-user API key, project ID, optional account ID and
|
|
96
|
+
environment (empty = PROD). A profile can also override the endpoints
|
|
97
|
+
(`api_url` / `hub_url`) for localhost or self-hosted installations.
|
|
98
|
+
`--profile` (or `NORBIX_PROFILE`) always uses exactly that profile and
|
|
99
|
+
ignores any login session — predictable for scripts.
|
|
63
100
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
3. **Config file** — written by `norbix login` and `norbix config set`
|
|
101
|
+
**Sessions** — `norbix login` stores a session in `~/.norbix/session.json`.
|
|
102
|
+
While it is valid, every command (in every terminal window) uses it.
|
|
103
|
+
`norbix logout` removes it. Profiles are untouched by login/logout.
|
|
68
104
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
105
|
+
Resolution order (most specific wins): flags → `NORBIX_*` env vars →
|
|
106
|
+
`--profile` (profile only) → session → `[default]` profile.
|
|
107
|
+
`norbix whoami` shows exactly what was resolved and verifies it against the
|
|
108
|
+
server. Defaults endpoints are `https://api.norbix.ai` and
|
|
109
|
+
`https://hub.norbix.ai`. See `AUTH_DESIGN.md` for the full design.
|
|
72
110
|
|
|
73
|
-
For CI/CD,
|
|
111
|
+
For CI/CD, use environment variables only:
|
|
74
112
|
|
|
75
113
|
```sh
|
|
76
114
|
export NORBIX_API_KEY=nbk_...
|
|
@@ -111,6 +149,103 @@ norbix files download invoices/2026/invoice.pdf
|
|
|
111
149
|
|
|
112
150
|
Or pass `--integration <id>` (env var: `NORBIX_FILES_INTEGRATION_ID`).
|
|
113
151
|
|
|
152
|
+
### The file commands
|
|
153
|
+
|
|
154
|
+
| Command | What it does |
|
|
155
|
+
| --- | --- |
|
|
156
|
+
| `norbix files list [path]` | List the files and folders under a path. No path lists the root. |
|
|
157
|
+
| `norbix files info <path>` | Show one file's details: size, type, when it changed. |
|
|
158
|
+
| `norbix files sign <path> [--expires <seconds>]` | Get a temporary web address for the file that anyone with the link can open. |
|
|
159
|
+
| `norbix files upload <local> [remote]` | Upload a file. Without `remote` it keeps its own name in the root folder. |
|
|
160
|
+
| `norbix files download <remote> [local]` | Download a file. Without `local` it keeps its own name in the current folder. |
|
|
161
|
+
| `norbix files delete <path> [--yes]` | Delete one file. Asks first unless you pass `--yes`. |
|
|
162
|
+
| `norbix files publish <path> [--folder]` | Give the file — or the whole folder — a link anyone can open, and print it. |
|
|
163
|
+
| `norbix files unpublish <path> [--folder]` | Take that link away again. |
|
|
164
|
+
| `norbix files integrations test <id>` | Check that the storage behind an integration really works. See below. |
|
|
165
|
+
|
|
166
|
+
Upload and download take more than one step, so the file bytes never pass
|
|
167
|
+
through Norbix:
|
|
168
|
+
|
|
169
|
+
* **upload** — ask for a short-lived upload address, send the bytes straight to
|
|
170
|
+
storage, then tell Norbix the upload finished.
|
|
171
|
+
* **download** — ask for a short-lived download address, then fetch the bytes
|
|
172
|
+
from storage.
|
|
173
|
+
|
|
174
|
+
`--content-type` overrides the type on upload; without it the type is guessed
|
|
175
|
+
from the file name.
|
|
176
|
+
|
|
177
|
+
### Publishing a file
|
|
178
|
+
|
|
179
|
+
`sign` and `publish` both give you a web address, and they are not the same
|
|
180
|
+
thing:
|
|
181
|
+
|
|
182
|
+
* **`sign`** hands out a *temporary* address that expires by itself. Good for a
|
|
183
|
+
download button on a page somebody is already signed in to.
|
|
184
|
+
* **`publish`** makes the file public *until you say otherwise*. The address
|
|
185
|
+
has no expiry and no sign-in: it works in an e-mail, in an `<img src>`, or in
|
|
186
|
+
a browser on somebody else's phone.
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
norbix files publish invoices/2026/invoice.pdf
|
|
190
|
+
# https://api.norbix.ai/v3/files/public/nbpf_7hK2abc/invoice.pdf
|
|
191
|
+
|
|
192
|
+
norbix files unpublish invoices/2026/invoice.pdf
|
|
193
|
+
# invoices/2026/invoice.pdf is private again — its link now gives a 404
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`--folder` does the same for a whole folder. That is **one** record however
|
|
197
|
+
many files sit under it, and every file inside becomes readable by putting its
|
|
198
|
+
path inside the folder after the link:
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
norbix files publish invoices --folder
|
|
202
|
+
# https://api.norbix.ai/v3/files/public/nbpf_folder1/
|
|
203
|
+
# → .../nbpf_folder1/2026/invoice.pdf
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Four rules worth knowing:
|
|
207
|
+
|
|
208
|
+
* Publishing the same thing twice gives the same link back — the first one is
|
|
209
|
+
already in somebody's hands.
|
|
210
|
+
* A file cannot be unpublished on its own while a folder above it is public.
|
|
211
|
+
The command says so and names the folder to switch off instead.
|
|
212
|
+
* Unpublishing a folder takes back every link inside it, per-file links
|
|
213
|
+
included.
|
|
214
|
+
* Every dead link answers the same plain `404`: unknown, renamed, unpublished,
|
|
215
|
+
deleted. A more precise answer would tell a stranger that the file is there.
|
|
216
|
+
|
|
217
|
+
### Testing an integration
|
|
218
|
+
|
|
219
|
+
`norbix files integrations test <id>` runs a live probe against the storage
|
|
220
|
+
behind the integration: it uploads a small file, reads it, lists its folder and
|
|
221
|
+
deletes it again. One line per step:
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
norbix files integrations test 55555555-5555-5555-5555-555555555555
|
|
225
|
+
# UploadFile OK
|
|
226
|
+
# GetFile FAILED
|
|
227
|
+
# - NoSuchKey: the object was not found
|
|
228
|
+
# GetAllFiles NOT_TESTED
|
|
229
|
+
# DeleteFile NOT_TESTED
|
|
230
|
+
# › Error: The files integration test failed: GetFile FAILED (2 later steps not tested).
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`NOT_TESTED` means the step was skipped because an earlier one failed. The
|
|
234
|
+
command exits `0` only when every step is `OK`, so it works in a script or a
|
|
235
|
+
CI job. `--json` prints the steps as data and keeps the same exit code. The
|
|
236
|
+
probe writes to the storage, so the key needs the `files:create` permission.
|
|
237
|
+
|
|
238
|
+
## Tests
|
|
239
|
+
|
|
240
|
+
```sh
|
|
241
|
+
npm test
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The tests build the CLI and run each command through oclif with `fetch`
|
|
245
|
+
replaced, so nothing leaves the machine and no account is needed. `HOME` points
|
|
246
|
+
at an empty temporary folder during the run, so your own `~/.norbix` settings
|
|
247
|
+
and login session are never read.
|
|
248
|
+
|
|
114
249
|
## Roadmap
|
|
115
250
|
|
|
116
251
|
- Standalone binaries (no Node needed): Homebrew, curl installer, Scoop/winget
|
package/dist/base.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { Norbix, NorbixError } from '@norbix.ai/ts';
|
|
2
2
|
import { Command, Flags } from '@oclif/core';
|
|
3
|
+
import { DEFAULT_API_URL, DEFAULT_HUB_URL, isSessionValid, readProfiles, readSession, } from './lib/profiles.js';
|
|
3
4
|
import { readStore } from './lib/store.js';
|
|
4
5
|
/**
|
|
5
6
|
* Base class for every Norbix CLI command.
|
|
6
7
|
*
|
|
7
|
-
* Resolution order
|
|
8
|
-
* 1. command-line
|
|
9
|
-
* 2. environment
|
|
10
|
-
*
|
|
8
|
+
* Resolution order (most specific wins):
|
|
9
|
+
* 1. command-line flags
|
|
10
|
+
* 2. environment variables (NORBIX_*)
|
|
11
|
+
* 3a. --profile / NORBIX_PROFILE set → that profile from ~/.norbix/config
|
|
12
|
+
* ONLY. The login session is intentionally ignored: --profile means
|
|
13
|
+
* "act as exactly this identity", good for scripts.
|
|
14
|
+
* 3b. no profile chosen → active login session (~/.norbix/session.json)
|
|
15
|
+
* wins; the [default] profile fills anything the session doesn't have;
|
|
16
|
+
* the legacy per-OS config.json is the last fallback.
|
|
11
17
|
*
|
|
12
|
-
*
|
|
18
|
+
* Default endpoints are https://api.norbix.ai and https://hub.norbix.ai;
|
|
19
|
+
* a profile can override them (self-hosted, localhost, custom domain).
|
|
13
20
|
*/
|
|
14
21
|
export class BaseCommand extends Command {
|
|
15
22
|
static enableJsonFlag = true;
|
|
@@ -30,7 +37,7 @@ export class BaseCommand extends Command {
|
|
|
30
37
|
helpGroup: 'GLOBAL',
|
|
31
38
|
}),
|
|
32
39
|
'api-key': Flags.string({
|
|
33
|
-
description: 'API key (overrides
|
|
40
|
+
description: 'API key (overrides profile and session)',
|
|
34
41
|
env: 'NORBIX_API_KEY',
|
|
35
42
|
helpGroup: 'GLOBAL',
|
|
36
43
|
}),
|
|
@@ -39,34 +46,96 @@ export class BaseCommand extends Command {
|
|
|
39
46
|
env: 'NORBIX_ACCOUNT_ID',
|
|
40
47
|
helpGroup: 'GLOBAL',
|
|
41
48
|
}),
|
|
49
|
+
profile: Flags.string({
|
|
50
|
+
description: 'Use this profile from ~/.norbix/config (ignores the login session)',
|
|
51
|
+
env: 'NORBIX_PROFILE',
|
|
52
|
+
helpGroup: 'GLOBAL',
|
|
53
|
+
}),
|
|
42
54
|
};
|
|
43
55
|
readStore() {
|
|
44
56
|
return readStore(this.config.configDir);
|
|
45
57
|
}
|
|
46
58
|
resolveContext(flags) {
|
|
47
|
-
const
|
|
59
|
+
const profiles = readProfiles();
|
|
60
|
+
const legacy = this.readStore();
|
|
61
|
+
const explicitProfile = flags.profile;
|
|
62
|
+
let prof = {};
|
|
63
|
+
let profileName;
|
|
64
|
+
let session;
|
|
65
|
+
if (explicitProfile) {
|
|
66
|
+
prof = profiles[explicitProfile] ?? {};
|
|
67
|
+
profileName = explicitProfile;
|
|
68
|
+
if (!profiles[explicitProfile]) {
|
|
69
|
+
this.error(`Profile "${explicitProfile}" not found in ~/.norbix/config.\n` +
|
|
70
|
+
`Run \`norbix configure --profile ${explicitProfile}\` to create it, or \`norbix profiles\` to list existing ones.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
prof = profiles.default ?? {};
|
|
75
|
+
profileName = profiles.default ? 'default' : undefined;
|
|
76
|
+
const s = readSession();
|
|
77
|
+
if (isSessionValid(s))
|
|
78
|
+
session = s;
|
|
79
|
+
}
|
|
48
80
|
const apiKeyOverride = flags['api-key'];
|
|
81
|
+
const bearerToken = apiKeyOverride ? undefined : session?.bearerToken;
|
|
82
|
+
const apiKey = apiKeyOverride ?? (bearerToken ? undefined : (prof.api_key ?? legacy.apiKey));
|
|
83
|
+
const authSource = apiKeyOverride
|
|
84
|
+
? 'flag/env api key'
|
|
85
|
+
: bearerToken
|
|
86
|
+
? 'session'
|
|
87
|
+
: prof.api_key
|
|
88
|
+
? `profile [${profileName ?? 'default'}]`
|
|
89
|
+
: legacy.apiKey
|
|
90
|
+
? 'legacy config'
|
|
91
|
+
: 'none';
|
|
92
|
+
const apiUrlBase = prof.api_url ?? (explicitProfile ? undefined : legacy.apiUrl) ?? DEFAULT_API_URL;
|
|
93
|
+
const hubUrlBase = prof.hub_url ?? (explicitProfile ? undefined : legacy.hubUrl) ?? DEFAULT_HUB_URL;
|
|
94
|
+
const region = flags.region ?? (explicitProfile ? prof.region : (session?.region ?? prof.region ?? legacy.region));
|
|
95
|
+
// Region subdomain (api.norbix.ai → nb-eu-germany.api.norbix.ai) is only
|
|
96
|
+
// composed for the default domains — a custom URL is never rewritten.
|
|
97
|
+
const withRegion = (url, isDefault) => isDefault && region ? url.replace('://', `://${region}.`) : url;
|
|
98
|
+
const usesDefaultEndpoints = apiUrlBase === DEFAULT_API_URL || hubUrlBase === DEFAULT_HUB_URL;
|
|
49
99
|
return {
|
|
50
|
-
|
|
51
|
-
projectId: flags.project ??
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
100
|
+
usesDefaultEndpoints,
|
|
101
|
+
projectId: flags.project ??
|
|
102
|
+
(explicitProfile ? prof.project_id : (session?.projectId ?? prof.project_id ?? legacy.projectId)),
|
|
103
|
+
accountId: flags.account ??
|
|
104
|
+
(explicitProfile ? prof.account_id : (session?.accountId ?? prof.account_id ?? legacy.accountId)),
|
|
105
|
+
env: flags.env ?? (explicitProfile ? prof.env : (session?.env ?? prof.env ?? legacy.env)),
|
|
106
|
+
region,
|
|
107
|
+
apiKey,
|
|
108
|
+
bearerToken,
|
|
109
|
+
apiUrl: withRegion(apiUrlBase, apiUrlBase === DEFAULT_API_URL),
|
|
110
|
+
hubUrl: withRegion(hubUrlBase, hubUrlBase === DEFAULT_HUB_URL),
|
|
111
|
+
filesIntegrationId: prof.files_integration_id ?? (explicitProfile ? undefined : legacy.filesIntegrationId),
|
|
112
|
+
authSource,
|
|
113
|
+
profileName,
|
|
114
|
+
userName: session?.userName,
|
|
115
|
+
stored: legacy,
|
|
60
116
|
};
|
|
61
117
|
}
|
|
62
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Rule: with the default *.norbix.ai endpoints a region is REQUIRED (the
|
|
120
|
+
* real endpoint is <region>.api.norbix.ai). With custom endpoints
|
|
121
|
+
* (localhost, self-hosted, custom domain) a region is optional.
|
|
122
|
+
*/
|
|
123
|
+
assertEndpoints(ctx) {
|
|
124
|
+
if (ctx.usesDefaultEndpoints && !ctx.region) {
|
|
125
|
+
this.error('Region is required when using the default norbix.ai endpoints.\n' +
|
|
126
|
+
'Set it with `norbix configure` (region field), pass --region <code> (e.g. nb-eu-germany),\n' +
|
|
127
|
+
'or set custom api_url / hub_url in the profile for self-hosted installations.');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Build an SDK client from the resolved context. Errors politely when auth is missing. */
|
|
63
131
|
client(flags, opts = {}) {
|
|
64
132
|
const ctx = this.resolveContext(flags);
|
|
133
|
+
this.assertEndpoints(ctx);
|
|
65
134
|
if (!ctx.projectId) {
|
|
66
|
-
this.error('No project ID configured.\nRun `norbix
|
|
135
|
+
this.error('No project ID configured.\nRun `norbix configure` (or `norbix login`), or pass --project.');
|
|
67
136
|
}
|
|
68
137
|
if (opts.requireAuth !== false && !ctx.apiKey && !ctx.bearerToken) {
|
|
69
|
-
this.error('Not authenticated
|
|
138
|
+
this.error('Not authenticated.\nRun `norbix login` (browser/user session) or `norbix configure` (API key profile), or pass --api-key.');
|
|
70
139
|
}
|
|
71
140
|
return new Norbix({
|
|
72
141
|
projectId: ctx.projectId,
|
|
@@ -75,6 +144,8 @@ export class BaseCommand extends Command {
|
|
|
75
144
|
bearerToken: ctx.bearerToken,
|
|
76
145
|
env: ctx.env,
|
|
77
146
|
region: ctx.region,
|
|
147
|
+
// Always explicit: CLI defaults are api/hub.norbix.ai (the SDK's own
|
|
148
|
+
// defaults still point at .dev — tracked as an SDK bug).
|
|
78
149
|
baseUrl: { api: ctx.apiUrl, hub: ctx.hubUrl },
|
|
79
150
|
},
|
|
80
151
|
// The CLI already resolved env vars itself — don't let the SDK re-read them.
|
|
@@ -90,9 +161,7 @@ export class BaseCommand extends Command {
|
|
|
90
161
|
if (error instanceof NorbixError) {
|
|
91
162
|
const status = error.status;
|
|
92
163
|
const suffix = status ? ` (HTTP ${status})` : '';
|
|
93
|
-
const hint = status === 401
|
|
94
|
-
? '\nYour session may have expired. Run `norbix login` again.'
|
|
95
|
-
: '';
|
|
164
|
+
const hint = status === 401 ? '\nYour session may have expired. Run `norbix login` again.' : '';
|
|
96
165
|
return this.error(`${error.message}${suffix}${hint}`);
|
|
97
166
|
}
|
|
98
167
|
return super.catch(error);
|
package/dist/commands/api.js
CHANGED
|
@@ -1,87 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const DEFAULT_HUB = 'https://hub.norbix.dev';
|
|
6
|
-
/**
|
|
7
|
-
* Raw escape hatch (like `gh api`): call ANY Norbix endpoint, including the
|
|
8
|
-
* ones this CLI has no dedicated command for yet. Sends the same auth and
|
|
9
|
-
* scope headers the SDK transport sends.
|
|
10
|
-
*/
|
|
11
|
-
export default class Api extends BaseCommand {
|
|
12
|
-
static description = `Call any Norbix endpoint directly (escape hatch, like \`gh api\`).
|
|
1
|
+
import { NamespaceCommand } from '../lib/namespace-command.js';
|
|
2
|
+
export default class ApiNs extends NamespaceCommand {
|
|
3
|
+
target = 'api';
|
|
4
|
+
static description = `Call any data-plane API endpoint with plain words.
|
|
13
5
|
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
Grammar: norbix api <module> <words...> [id] [--field value]
|
|
7
|
+
Same engine as \`norbix hub\` — see \`norbix hub --help\`. For raw HTTP paths
|
|
8
|
+
use \`norbix raw\`.`;
|
|
9
|
+
static strict = false;
|
|
16
10
|
static examples = [
|
|
17
|
-
'<%= config.bin %> api
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
'<%= config.bin %> api # list modules',
|
|
12
|
+
'<%= config.bin %> api database # list database methods',
|
|
13
|
+
'<%= config.bin %> api database find --collectionName orders --pageSize 20',
|
|
14
|
+
'<%= config.bin %> api membership users get',
|
|
20
15
|
];
|
|
21
|
-
static args = {
|
|
22
|
-
path: Args.string({ required: true, description: 'Endpoint path, e.g. /v2/logs or /{version}/logs' }),
|
|
23
|
-
};
|
|
24
|
-
static flags = {
|
|
25
|
-
hub: Flags.boolean({ description: 'Call the Hub service instead of the API service', default: false }),
|
|
26
|
-
method: Flags.string({ char: 'X', description: 'HTTP method', default: 'GET', options: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] }),
|
|
27
|
-
body: Flags.string({ char: 'b', description: 'JSON request body (or `-` for stdin)' }),
|
|
28
|
-
query: Flags.string({ char: 'q', description: 'Query parameter key=value (repeatable)', multiple: true }),
|
|
29
|
-
};
|
|
30
16
|
async run() {
|
|
31
|
-
const {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
this.error('No project ID configured. Run `norbix login` or pass --project.');
|
|
35
|
-
const token = ctx.bearerToken ?? ctx.apiKey;
|
|
36
|
-
if (!token)
|
|
37
|
-
this.error('Not authenticated. Run `norbix login` or pass --api-key.');
|
|
38
|
-
// Base URL: custom > default (+ region subdomain, same rule as the SDK).
|
|
39
|
-
let base = flags.hub ? (ctx.hubUrl ?? DEFAULT_HUB) : (ctx.apiUrl ?? DEFAULT_API);
|
|
40
|
-
const usesDefault = flags.hub ? !ctx.hubUrl : !ctx.apiUrl;
|
|
41
|
-
if (usesDefault && ctx.region)
|
|
42
|
-
base = base.replace('://', `://${ctx.region}.`);
|
|
43
|
-
const path = args.path.replace('{version}', 'v2');
|
|
44
|
-
const url = new URL(base.replace(/\/$/, '') + (path.startsWith('/') ? path : `/${path}`));
|
|
45
|
-
for (const pair of flags.query ?? []) {
|
|
46
|
-
const eq = pair.indexOf('=');
|
|
47
|
-
if (eq < 1)
|
|
48
|
-
this.error(`--query must be key=value, got "${pair}"`);
|
|
49
|
-
url.searchParams.append(pair.slice(0, eq), pair.slice(eq + 1));
|
|
50
|
-
}
|
|
51
|
-
const headers = {
|
|
52
|
-
Accept: 'application/json',
|
|
53
|
-
Authorization: `Bearer ${token}`,
|
|
54
|
-
'norbix-project-id': ctx.projectId,
|
|
55
|
-
'X-CM-ProjectId': ctx.projectId,
|
|
56
|
-
};
|
|
57
|
-
if (ctx.accountId) {
|
|
58
|
-
headers['norbix-account-id'] = ctx.accountId;
|
|
59
|
-
headers['X-CM-AccountId'] = ctx.accountId;
|
|
60
|
-
}
|
|
61
|
-
if (ctx.env && ctx.env !== 'PROD')
|
|
62
|
-
headers['norbix-env'] = ctx.env;
|
|
63
|
-
if (ctx.region)
|
|
64
|
-
headers['nb-region'] = ctx.region;
|
|
65
|
-
let body;
|
|
66
|
-
if (flags.body) {
|
|
67
|
-
body = await readJsonInput(flags.body, 'body');
|
|
68
|
-
headers['Content-Type'] = 'application/json';
|
|
69
|
-
}
|
|
70
|
-
const res = await fetch(url, { method: flags.method, headers, body });
|
|
71
|
-
const text = await res.text();
|
|
72
|
-
let data;
|
|
73
|
-
try {
|
|
74
|
-
data = JSON.parse(text);
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
data = text;
|
|
78
|
-
}
|
|
79
|
-
if (!res.ok) {
|
|
80
|
-
this.warn(`HTTP ${res.status} ${res.statusText}`);
|
|
81
|
-
this.print(data);
|
|
82
|
-
this.exit(1);
|
|
83
|
-
}
|
|
84
|
-
this.print(data);
|
|
85
|
-
return data;
|
|
17
|
+
const { flagArgv, rest, help } = this.splitArgv();
|
|
18
|
+
const { flags } = await this.parse(ApiNs, flagArgv);
|
|
19
|
+
return this.dispatch(rest, flags, help);
|
|
86
20
|
}
|
|
87
21
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { input, password as passwordPrompt } from '@inquirer/prompts';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../base.js';
|
|
4
|
+
import { DEFAULT_API_URL, DEFAULT_HUB_URL, PROFILES_PATH, readProfiles, writeProfile, } from '../lib/profiles.js';
|
|
5
|
+
import { redact } from '../lib/store.js';
|
|
6
|
+
export default class Configure extends BaseCommand {
|
|
7
|
+
static description = `Set up a profile in ~/.norbix/config (like \`aws configure\`).
|
|
8
|
+
|
|
9
|
+
Asks for a service-user API key, project ID, and optional account ID and
|
|
10
|
+
environment. Use --profile to create or edit a named profile; without it the
|
|
11
|
+
[default] profile is written. Later, run any command with
|
|
12
|
+
--profile <name> to use exactly that profile.`;
|
|
13
|
+
static examples = [
|
|
14
|
+
'<%= config.bin %> configure',
|
|
15
|
+
'<%= config.bin %> configure --profile fitskin-prod',
|
|
16
|
+
'<%= config.bin %> configure --profile localhost --api-url http://localhost:5001 --hub-url http://localhost:5002',
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
'api-url': Flags.string({ description: `API endpoint (default: ${DEFAULT_API_URL})` }),
|
|
20
|
+
'hub-url': Flags.string({ description: `Hub endpoint (default: ${DEFAULT_HUB_URL})` }),
|
|
21
|
+
};
|
|
22
|
+
async run() {
|
|
23
|
+
const { flags } = await this.parse(Configure);
|
|
24
|
+
const name = flags.profile ?? 'default';
|
|
25
|
+
const existing = readProfiles()[name] ?? {};
|
|
26
|
+
this.log(`Configuring profile [${name}] in ${PROFILES_PATH}`);
|
|
27
|
+
// Same interaction style as `aws configure`: show the current value
|
|
28
|
+
// (redacted for the key), keep it when the user just presses ENTER.
|
|
29
|
+
const keyLabel = existing.api_key
|
|
30
|
+
? `Service user API key [${redact(existing.api_key)}]:`
|
|
31
|
+
: 'Service user API key:';
|
|
32
|
+
const apiKeyInput = await passwordPrompt({ message: keyLabel, mask: '*' });
|
|
33
|
+
const apiKey = apiKeyInput || existing.api_key;
|
|
34
|
+
const projectId = (await input({ message: 'Project ID:', default: existing.project_id })) || undefined;
|
|
35
|
+
const accountId = (await input({
|
|
36
|
+
message: 'Account ID (optional, for account-level commands):',
|
|
37
|
+
default: existing.account_id ?? '',
|
|
38
|
+
})) || undefined;
|
|
39
|
+
const env = (await input({
|
|
40
|
+
message: 'Environment (optional, empty = PROD):',
|
|
41
|
+
default: existing.env ?? '',
|
|
42
|
+
})) || undefined;
|
|
43
|
+
// With the default norbix.ai endpoints the region is REQUIRED (requests
|
|
44
|
+
// go to <region>.api.norbix.ai). With custom endpoints it is optional.
|
|
45
|
+
const usesDefaultEndpoints = !(flags['api-url'] ?? existing.api_url) && !(flags['hub-url'] ?? existing.hub_url);
|
|
46
|
+
const region = (await input({
|
|
47
|
+
message: usesDefaultEndpoints
|
|
48
|
+
? 'Region (REQUIRED, e.g. nb-eu-germany):'
|
|
49
|
+
: 'Region (optional with custom endpoints):',
|
|
50
|
+
default: existing.region ?? '',
|
|
51
|
+
validate: (v) => (!usesDefaultEndpoints || v.trim() ? true : 'Region is required when using the default norbix.ai endpoints.'),
|
|
52
|
+
})) || undefined;
|
|
53
|
+
writeProfile(name, {
|
|
54
|
+
api_key: apiKey,
|
|
55
|
+
project_id: projectId,
|
|
56
|
+
account_id: accountId,
|
|
57
|
+
env,
|
|
58
|
+
region,
|
|
59
|
+
api_url: flags['api-url'] ?? existing.api_url,
|
|
60
|
+
hub_url: flags['hub-url'] ?? existing.hub_url,
|
|
61
|
+
files_integration_id: existing.files_integration_id,
|
|
62
|
+
});
|
|
63
|
+
this.log(`Profile [${name}] saved.`);
|
|
64
|
+
if (name !== 'default')
|
|
65
|
+
this.log(`Use it with: norbix <command> --profile ${name}`);
|
|
66
|
+
return { profile: name, saved: true };
|
|
67
|
+
}
|
|
68
|
+
}
|
package/dist/commands/env/use.js
CHANGED
|
@@ -1,17 +1,46 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../../base.js';
|
|
3
|
+
import { isSessionValid, readProfiles, readSession, writeProfile, writeSession } from '../../lib/profiles.js';
|
|
3
4
|
import { writeStore } from '../../lib/store.js';
|
|
4
5
|
export default class EnvUse extends BaseCommand {
|
|
5
|
-
static description =
|
|
6
|
-
|
|
6
|
+
static description = `Set the default environment for future commands.
|
|
7
|
+
|
|
8
|
+
Written to the active session when you are logged in; with --profile it is
|
|
9
|
+
written into that profile in ~/.norbix/config; otherwise to the [default]
|
|
10
|
+
profile. Override per command with --env.`;
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> env use TEST',
|
|
13
|
+
'<%= config.bin %> env use PROD',
|
|
14
|
+
'<%= config.bin %> env use STAGING --profile fitskin-prod',
|
|
15
|
+
];
|
|
7
16
|
static args = {
|
|
8
17
|
name: Args.string({ required: true, description: 'Environment name (e.g. PROD, TEST)' }),
|
|
9
18
|
};
|
|
10
19
|
async run() {
|
|
11
|
-
const { args } = await this.parse(EnvUse);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
const { args, flags } = await this.parse(EnvUse);
|
|
21
|
+
if (flags.profile) {
|
|
22
|
+
const prof = readProfiles()[flags.profile];
|
|
23
|
+
if (!prof)
|
|
24
|
+
this.error(`Profile "${flags.profile}" not found. Run \`norbix configure --profile ${flags.profile}\` first.`);
|
|
25
|
+
writeProfile(flags.profile, { ...prof, env: args.name });
|
|
26
|
+
this.print(`Profile [${flags.profile}] now defaults to environment ${args.name}.`);
|
|
27
|
+
return { env: args.name, profile: flags.profile };
|
|
28
|
+
}
|
|
29
|
+
const session = readSession();
|
|
30
|
+
if (isSessionValid(session)) {
|
|
31
|
+
writeSession({ ...session, env: args.name });
|
|
32
|
+
this.print(`Session now targets environment ${args.name}.`);
|
|
33
|
+
return { env: args.name, target: 'session' };
|
|
34
|
+
}
|
|
35
|
+
const prof = readProfiles().default;
|
|
36
|
+
if (prof) {
|
|
37
|
+
writeProfile('default', { ...prof, env: args.name });
|
|
38
|
+
this.print(`Profile [default] now defaults to environment ${args.name}.`);
|
|
39
|
+
return { env: args.name, profile: 'default' };
|
|
40
|
+
}
|
|
41
|
+
// Last resort: legacy config location.
|
|
42
|
+
writeStore(this.config.configDir, { ...this.readStore(), env: args.name });
|
|
43
|
+
this.print(`Default environment set to ${args.name}.`);
|
|
44
|
+
return { env: args.name, target: 'legacy' };
|
|
16
45
|
}
|
|
17
46
|
}
|
|
@@ -15,7 +15,7 @@ export default class FilesDelete extends BaseCommand {
|
|
|
15
15
|
async run() {
|
|
16
16
|
const { args, flags } = await this.parse(FilesDelete);
|
|
17
17
|
const client = this.client(flags);
|
|
18
|
-
const integration = resolveIntegration(flags.integration, this.
|
|
18
|
+
const integration = resolveIntegration(flags.integration, this.resolveContext(flags).filesIntegrationId);
|
|
19
19
|
if (!integration) {
|
|
20
20
|
this.error('No files integration ID. Pass --integration or run `norbix config set filesIntegrationId <id>`.');
|
|
21
21
|
}
|
|
@@ -22,7 +22,7 @@ export default class FilesDownload extends BaseCommand {
|
|
|
22
22
|
async run() {
|
|
23
23
|
const { args, flags } = await this.parse(FilesDownload);
|
|
24
24
|
const client = this.client(flags);
|
|
25
|
-
const integration = resolveIntegration(flags.integration, this.
|
|
25
|
+
const integration = resolveIntegration(flags.integration, this.resolveContext(flags).filesIntegrationId);
|
|
26
26
|
if (!integration) {
|
|
27
27
|
this.error('No files integration ID. Pass --integration or run `norbix config set filesIntegrationId <id>`.');
|
|
28
28
|
}
|
|
@@ -13,7 +13,7 @@ export default class FilesInfo extends BaseCommand {
|
|
|
13
13
|
async run() {
|
|
14
14
|
const { args, flags } = await this.parse(FilesInfo);
|
|
15
15
|
const client = this.client(flags);
|
|
16
|
-
const integration = resolveIntegration(flags.integration, this.
|
|
16
|
+
const integration = resolveIntegration(flags.integration, this.resolveContext(flags).filesIntegrationId);
|
|
17
17
|
if (!integration) {
|
|
18
18
|
this.error('No files integration ID. Pass --integration or run `norbix config set filesIntegrationId <id>`.');
|
|
19
19
|
}
|