@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.
Files changed (43) hide show
  1. package/README.md +148 -13
  2. package/dist/base.js +92 -23
  3. package/dist/commands/api.js +15 -81
  4. package/dist/commands/configure.js +68 -0
  5. package/dist/commands/env/use.js +36 -7
  6. package/dist/commands/files/delete.js +1 -1
  7. package/dist/commands/files/download.js +1 -1
  8. package/dist/commands/files/info.js +1 -1
  9. package/dist/commands/files/integrations/test.js +85 -0
  10. package/dist/commands/files/list.js +1 -1
  11. package/dist/commands/files/publish.js +38 -0
  12. package/dist/commands/files/sign.js +1 -1
  13. package/dist/commands/files/unpublish.js +34 -0
  14. package/dist/commands/files/upload.js +1 -1
  15. package/dist/commands/hub.js +24 -0
  16. package/dist/commands/login.js +82 -36
  17. package/dist/commands/logout.js +11 -13
  18. package/dist/commands/profiles.js +25 -0
  19. package/dist/commands/push/archive.js +4 -1
  20. package/dist/commands/push/clone.js +4 -1
  21. package/dist/commands/push/delete.js +4 -1
  22. package/dist/commands/push/device/register.js +35 -0
  23. package/dist/commands/push/integration/default.js +19 -0
  24. package/dist/commands/push/integration/delete.js +28 -0
  25. package/dist/commands/push/integration/disable.js +19 -0
  26. package/dist/commands/push/integration/enable.js +19 -0
  27. package/dist/commands/push/integration/test.js +30 -0
  28. package/dist/commands/push/integration.js +16 -0
  29. package/dist/commands/push/integrations.js +20 -0
  30. package/dist/commands/push/preview.js +19 -0
  31. package/dist/commands/push/settings.js +16 -0
  32. package/dist/commands/push/unarchive.js +4 -1
  33. package/dist/commands/raw.js +86 -0
  34. package/dist/commands/whoami.js +25 -8
  35. package/dist/help.js +18 -0
  36. package/dist/lib/device-login.js +76 -0
  37. package/dist/lib/dispatch.js +151 -0
  38. package/dist/lib/filesIntegrationTest.js +57 -0
  39. package/dist/lib/namespace-command.js +148 -0
  40. package/dist/lib/profiles.js +138 -0
  41. package/dist/lib/publicFiles.js +63 -0
  42. package/oclif.manifest.json +3272 -737
  43. 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 18+).
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 manage files |
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 api <path>` | Call ANY endpoint directly (like `gh api`) — full API coverage |
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
- Every setting is resolved in this order (most specific wins):
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
- 1. **Flag** — `--project`, `--env`, `--region`, `--api-key`
65
- 2. **Environment variable** — `NORBIX_PROJECT_ID`, `NORBIX_ENV`, `NORBIX_REGION`,
66
- `NORBIX_API_KEY`, `NORBIX_API_URL`, `NORBIX_HUB_URL`
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
- The config file lives in your user config folder (`~/.config/norbix/config.json`
70
- on macOS/Linux, `%LOCALAPPDATA%\norbix\config.json` on Windows) and is created
71
- with owner-only permissions (0600).
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, skip `login` and use environment variables only:
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 for every setting (most specific wins):
8
- * 1. command-line flag (--project, --env, --region, --api-key)
9
- * 2. environment variable (NORBIX_PROJECT_ID, NORBIX_ENV, ...)
10
- * 3. local config file (written by `norbix login` / `norbix config set`)
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
- * Flags declare `env:` so oclif applies (1) and (2); (3) happens here.
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 a stored login token)',
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 stored = this.readStore();
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
- stored,
51
- projectId: flags.project ?? stored.projectId,
52
- accountId: flags.account ?? stored.accountId,
53
- env: flags.env ?? stored.env,
54
- region: flags.region ?? stored.region,
55
- apiKey: apiKeyOverride ?? (stored.bearerToken ? undefined : stored.apiKey),
56
- // An explicit --api-key / NORBIX_API_KEY wins over a stored login token.
57
- bearerToken: apiKeyOverride ? undefined : stored.bearerToken,
58
- apiUrl: stored.apiUrl ?? process.env.NORBIX_API_URL,
59
- hubUrl: stored.hubUrl ?? process.env.NORBIX_HUB_URL,
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
- /** Build an SDK client from flags + env + config file. Errors politely when auth is missing. */
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 login`, or `norbix config set projectId <id>`, or pass --project.');
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. Run `norbix login` or pass --api-key.');
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);
@@ -1,87 +1,21 @@
1
- import { Args, Flags } from '@oclif/core';
2
- import { BaseCommand } from '../base.js';
3
- import { readJsonInput } from '../lib/json.js';
4
- const DEFAULT_API = 'https://api.norbix.dev';
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
- Sends your auth token plus project/account/env/region headers automatically.
15
- Use {version} in the path and it becomes v2.`;
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 /v2/logs/settings --hub',
18
- `<%= config.bin %> api '/{version}/database/collections/orders/count' --query filter='{"status":"paid"}'`,
19
- `<%= config.bin %> api /v2/scheduler/tasks --hub --method POST --body '{"name":"nightly",...}'`,
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 { args, flags } = await this.parse(Api);
32
- const ctx = this.resolveContext(flags);
33
- if (!ctx.projectId)
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
+ }
@@ -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 = 'Set the default environment for all future commands (stored locally)';
6
- static examples = ['<%= config.bin %> env use TEST', '<%= config.bin %> env use PROD'];
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
- const stored = this.readStore();
13
- writeStore(this.config.configDir, { ...stored, env: args.name });
14
- this.print(`Default environment set to ${args.name}. Override per command with --env.`);
15
- return { env: args.name };
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.readStore().filesIntegrationId);
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.readStore().filesIntegrationId);
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.readStore().filesIntegrationId);
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
  }