@norbix.ai/cli 0.1.0 → 0.2.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 (41) hide show
  1. package/README.md +125 -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/list.js +1 -1
  10. package/dist/commands/files/publish.js +38 -0
  11. package/dist/commands/files/sign.js +1 -1
  12. package/dist/commands/files/unpublish.js +34 -0
  13. package/dist/commands/files/upload.js +1 -1
  14. package/dist/commands/hub.js +24 -0
  15. package/dist/commands/login.js +82 -36
  16. package/dist/commands/logout.js +11 -13
  17. package/dist/commands/profiles.js +25 -0
  18. package/dist/commands/push/archive.js +4 -1
  19. package/dist/commands/push/clone.js +4 -1
  20. package/dist/commands/push/delete.js +4 -1
  21. package/dist/commands/push/device/register.js +35 -0
  22. package/dist/commands/push/integration/default.js +19 -0
  23. package/dist/commands/push/integration/delete.js +28 -0
  24. package/dist/commands/push/integration/disable.js +19 -0
  25. package/dist/commands/push/integration/enable.js +19 -0
  26. package/dist/commands/push/integration/test.js +30 -0
  27. package/dist/commands/push/integration.js +16 -0
  28. package/dist/commands/push/integrations.js +20 -0
  29. package/dist/commands/push/preview.js +19 -0
  30. package/dist/commands/push/settings.js +16 -0
  31. package/dist/commands/push/unarchive.js +4 -1
  32. package/dist/commands/raw.js +86 -0
  33. package/dist/commands/whoami.js +25 -8
  34. package/dist/help.js +18 -0
  35. package/dist/lib/device-login.js +76 -0
  36. package/dist/lib/dispatch.js +151 -0
  37. package/dist/lib/namespace-command.js +148 -0
  38. package/dist/lib/profiles.js +138 -0
  39. package/dist/lib/publicFiles.js +63 -0
  40. package/oclif.manifest.json +3166 -725
  41. package/package.json +24 -12
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,7 @@ 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
43
  | `norbix users list/get/invite/block/unblock/delete` | Manage project users (membership) |
44
44
  | `norbix env list/use/delete` | Manage project environments |
45
45
  | `norbix logs list/trail` | Read project logs, follow one correlation ID |
@@ -52,25 +52,62 @@ norbix scheduler list
52
52
  | `norbix payments integrations/triggers/trigger/enable/disable` | Payment integrations and triggers |
53
53
  | `norbix integrations <module>` | List integrations of any module |
54
54
  | `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 |
55
+ | `norbix hub <module> <words...>` | Call ANY hub endpoint with plain words (see below) |
56
+ | `norbix api <module> <words...>` | Same for the data-plane API |
57
+ | `norbix raw <path>` | Low-level HTTP escape hatch (hub by default, `--api` for API) |
58
+
59
+ ### Plain-word access to every endpoint
60
+
61
+ `norbix hub` and `norbix api` resolve SDK methods from plain words — every
62
+ current and future SDK method is callable without waiting for a dedicated
63
+ command:
64
+
65
+ ```sh
66
+ norbix hub # list modules
67
+ norbix hub database # list database methods
68
+ norbix hub database aggregates get # plural = list
69
+ norbix hub database aggregate get maggr_123 # singular = one item
70
+ norbix hub database aggregates delete maggr_123 --schemaId sch_456
71
+ norbix hub scheduler tasks get --pageSize 100
72
+ norbix hub email templates get # email/sms/push route into notifications
73
+ ```
74
+
75
+ The first positional value becomes `id`; `--field value` flags become request
76
+ fields. Destructive verbs (delete, remove, rotate, ...) ask for confirmation
77
+ unless `--yes`. Add `--dry-run` to preview the exact SDK call.
56
78
  | `norbix autocomplete` | Set up shell tab-completion (bash/zsh) |
57
79
 
58
80
  Run `norbix <topic> --help` for flags and examples.
59
81
 
60
- ## Configuration
82
+ ## Configuration: profiles + sessions
83
+
84
+ Two ways to authenticate, and they work together:
85
+
86
+ **Profiles (AWS-style)** — one INI file at `~/.norbix/config`, set up with:
87
+
88
+ ```sh
89
+ norbix configure # writes the [default] profile
90
+ norbix configure --profile fitskin # a named profile
91
+ norbix db find orders --profile fitskin
92
+ ```
61
93
 
62
- Every setting is resolved in this order (most specific wins):
94
+ It asks for a service-user API key, project ID, optional account ID and
95
+ environment (empty = PROD). A profile can also override the endpoints
96
+ (`api_url` / `hub_url`) for localhost or self-hosted installations.
97
+ `--profile` (or `NORBIX_PROFILE`) always uses exactly that profile and
98
+ ignores any login session — predictable for scripts.
63
99
 
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`
100
+ **Sessions** — `norbix login` stores a session in `~/.norbix/session.json`.
101
+ While it is valid, every command (in every terminal window) uses it.
102
+ `norbix logout` removes it. Profiles are untouched by login/logout.
68
103
 
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).
104
+ Resolution order (most specific wins): flags → `NORBIX_*` env vars →
105
+ `--profile` (profile only) → session → `[default]` profile.
106
+ `norbix whoami` shows exactly what was resolved and verifies it against the
107
+ server. Defaults endpoints are `https://api.norbix.ai` and
108
+ `https://hub.norbix.ai`. See `AUTH_DESIGN.md` for the full design.
72
109
 
73
- For CI/CD, skip `login` and use environment variables only:
110
+ For CI/CD, use environment variables only:
74
111
 
75
112
  ```sh
76
113
  export NORBIX_API_KEY=nbk_...
@@ -111,6 +148,81 @@ norbix files download invoices/2026/invoice.pdf
111
148
 
112
149
  Or pass `--integration <id>` (env var: `NORBIX_FILES_INTEGRATION_ID`).
113
150
 
151
+ ### The eight file commands
152
+
153
+ | Command | What it does |
154
+ | --- | --- |
155
+ | `norbix files list [path]` | List the files and folders under a path. No path lists the root. |
156
+ | `norbix files info <path>` | Show one file's details: size, type, when it changed. |
157
+ | `norbix files sign <path> [--expires <seconds>]` | Get a temporary web address for the file that anyone with the link can open. |
158
+ | `norbix files upload <local> [remote]` | Upload a file. Without `remote` it keeps its own name in the root folder. |
159
+ | `norbix files download <remote> [local]` | Download a file. Without `local` it keeps its own name in the current folder. |
160
+ | `norbix files delete <path> [--yes]` | Delete one file. Asks first unless you pass `--yes`. |
161
+ | `norbix files publish <path> [--folder]` | Give the file — or the whole folder — a link anyone can open, and print it. |
162
+ | `norbix files unpublish <path> [--folder]` | Take that link away again. |
163
+
164
+ Upload and download take more than one step, so the file bytes never pass
165
+ through Norbix:
166
+
167
+ * **upload** — ask for a short-lived upload address, send the bytes straight to
168
+ storage, then tell Norbix the upload finished.
169
+ * **download** — ask for a short-lived download address, then fetch the bytes
170
+ from storage.
171
+
172
+ `--content-type` overrides the type on upload; without it the type is guessed
173
+ from the file name.
174
+
175
+ ### Publishing a file
176
+
177
+ `sign` and `publish` both give you a web address, and they are not the same
178
+ thing:
179
+
180
+ * **`sign`** hands out a *temporary* address that expires by itself. Good for a
181
+ download button on a page somebody is already signed in to.
182
+ * **`publish`** makes the file public *until you say otherwise*. The address
183
+ has no expiry and no sign-in: it works in an e-mail, in an `<img src>`, or in
184
+ a browser on somebody else's phone.
185
+
186
+ ```sh
187
+ norbix files publish invoices/2026/invoice.pdf
188
+ # https://api.norbix.ai/v3/files/public/nbpf_7hK2abc/invoice.pdf
189
+
190
+ norbix files unpublish invoices/2026/invoice.pdf
191
+ # invoices/2026/invoice.pdf is private again — its link now gives a 404
192
+ ```
193
+
194
+ `--folder` does the same for a whole folder. That is **one** record however
195
+ many files sit under it, and every file inside becomes readable by putting its
196
+ path inside the folder after the link:
197
+
198
+ ```sh
199
+ norbix files publish invoices --folder
200
+ # https://api.norbix.ai/v3/files/public/nbpf_folder1/
201
+ # → .../nbpf_folder1/2026/invoice.pdf
202
+ ```
203
+
204
+ Four rules worth knowing:
205
+
206
+ * Publishing the same thing twice gives the same link back — the first one is
207
+ already in somebody's hands.
208
+ * A file cannot be unpublished on its own while a folder above it is public.
209
+ The command says so and names the folder to switch off instead.
210
+ * Unpublishing a folder takes back every link inside it, per-file links
211
+ included.
212
+ * Every dead link answers the same plain `404`: unknown, renamed, unpublished,
213
+ deleted. A more precise answer would tell a stranger that the file is there.
214
+
215
+ ## Tests
216
+
217
+ ```sh
218
+ npm test
219
+ ```
220
+
221
+ The tests build the CLI and run each command through oclif with `fetch`
222
+ replaced, so nothing leaves the machine and no account is needed. `HOME` points
223
+ at an empty temporary folder during the run, so your own `~/.norbix` settings
224
+ and login session are never read.
225
+
114
226
  ## Roadmap
115
227
 
116
228
  - 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
  }
@@ -16,7 +16,7 @@ export default class FilesList extends BaseCommand {
16
16
  async run() {
17
17
  const { args, flags } = await this.parse(FilesList);
18
18
  const client = this.client(flags);
19
- const integration = resolveIntegration(flags.integration, this.readStore().filesIntegrationId);
19
+ const integration = resolveIntegration(flags.integration, this.resolveContext(flags).filesIntegrationId);
20
20
  if (!integration) {
21
21
  this.error('No files integration ID. Pass --integration or run `norbix config set filesIntegrationId <id>`.');
22
22
  }
@@ -0,0 +1,38 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base.js';
3
+ import { integrationFlag, resolveIntegration } from '../../lib/files.js';
4
+ import { callPublicFiles, publicUrlFor } from '../../lib/publicFiles.js';
5
+ export default class FilesPublish extends BaseCommand {
6
+ static args = {
7
+ remote: Args.string({ description: 'Remote file path, or folder prefix with --folder', required: true }),
8
+ };
9
+ static description = 'Make a file (or a whole folder) readable by anyone holding its link — no sign-in needed';
10
+ static examples = [
11
+ '<%= config.bin %> files publish invoices/2026/invoice.pdf',
12
+ '<%= config.bin %> files publish invoices --folder',
13
+ ];
14
+ static flags = {
15
+ folder: Flags.boolean({
16
+ description: 'Publish the whole folder prefix instead of one file',
17
+ }),
18
+ integration: integrationFlag,
19
+ };
20
+ async run() {
21
+ const { args, flags } = await this.parse(FilesPublish);
22
+ const ctx = this.resolveContext(flags);
23
+ // Builds nothing itself, but it is what refuses early and with a useful
24
+ // sentence when there is no project or no credentials.
25
+ this.client(flags);
26
+ const integration = resolveIntegration(flags.integration, ctx.filesIntegrationId);
27
+ if (!integration) {
28
+ this.error('No files integration ID. Pass --integration or run `norbix config set filesIntegrationId <id>`.');
29
+ }
30
+ const result = await callPublicFiles(ctx, flags.folder ? 'makeFolderPublic' : 'makeFilePublic', { filesIntegrationId: integration, path: args.remote });
31
+ if (!result.id) {
32
+ this.error('The gateway did not return a public id.');
33
+ }
34
+ const url = publicUrlFor(ctx.apiUrl, result.id, args.remote, flags.folder);
35
+ this.print(url);
36
+ return { publicId: result.id, publicUrl: url };
37
+ }
38
+ }