@norbix.ai/cli 1.1.0 → 1.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.
package/README.md CHANGED
@@ -149,6 +149,15 @@ came back negative (`norbix files integrations test`).
149
149
  not before — `norbix files publish` / `unpublish` were the two commands that
150
150
  used to treat it as a success.
151
151
 
152
+ ## Versioning
153
+
154
+ The major version is frozen at **v1** until the public launch.
155
+
156
+ - A breaking change is released as a **minor** (for example v1.2.0 → v1.3.0), never as a new major.
157
+ - Write it as `feat(<scope>): <what>` and add a line `Breaking: <what changed and what callers must do>` in plain words, in the pull-request body and in the commit message.
158
+ - Never mark it the conventional-commits way: no `!` in the title (`feat!:`), no BREAKING CHANGE footer. The `PR title` check fails a pull request that does.
159
+ - As a safety net, the release config (`.releaserc.json` → `releaseRules`) maps breaking commits to a minor, so one that slips through still does not bump the major.
160
+
152
161
  ## Development
153
162
 
154
163
  ```sh
package/dist/base.js CHANGED
@@ -2,6 +2,16 @@ import { Norbix, NorbixError } from '@norbix.ai/ts';
2
2
  import { Command, Flags } from '@oclif/core';
3
3
  import { DEFAULT_API_URL, DEFAULT_HUB_URL, isSessionValid, readProfiles, readSession, } from './lib/profiles.js';
4
4
  import { readStore } from './lib/store.js';
5
+ /** Stands in for a projectId the SDK constructor insists on; never sent. */
6
+ const NO_PROJECT_PLACEHOLDER = 'no-project';
7
+ /** Removes the project headers the SDK adds for the placeholder projectId. */
8
+ export const dropProjectHeaders = async (ctx) => {
9
+ const headers = new Headers(ctx.init.headers);
10
+ headers.delete('norbix-project-id');
11
+ headers.delete('X-CM-ProjectId');
12
+ ctx.init.headers = headers;
13
+ return ctx.next();
14
+ };
5
15
  /**
6
16
  * Base class for every Norbix CLI command.
7
17
  *
@@ -127,18 +137,28 @@ export class BaseCommand extends Command {
127
137
  'or set custom api_url / hub_url in the profile for self-hosted installations.');
128
138
  }
129
139
  }
130
- /** Build an SDK client from the resolved context. Errors politely when auth is missing. */
140
+ /**
141
+ * Build an SDK client from the resolved context. Errors politely when auth is missing.
142
+ *
143
+ * `requireAuth: false` — the call may go out without a login (a signed link is the key);
144
+ * a login, when there is one, is still used.
145
+ * `requireProject: false` — the call does not need a project (a signed link carries it).
146
+ * The SDK constructor still demands a projectId, so a placeholder is passed and the
147
+ * project headers are taken off the wire again: the gateway never sees a fake project.
148
+ */
131
149
  client(flags, opts = {}) {
132
150
  const ctx = this.resolveContext(flags);
133
151
  this.assertEndpoints(ctx);
134
- if (!ctx.projectId) {
152
+ const noProject = !ctx.projectId && opts.requireProject === false;
153
+ if (!ctx.projectId && !noProject) {
135
154
  this.error('No project ID configured.\nRun `norbix configure` (or `norbix login`), or pass --project.');
136
155
  }
137
156
  if (opts.requireAuth !== false && !ctx.apiKey && !ctx.bearerToken) {
138
157
  this.error('Not authenticated.\nRun `norbix login` (browser/user session) or `norbix configure` (API key profile), or pass --api-key.');
139
158
  }
140
159
  return new Norbix({
141
- projectId: ctx.projectId,
160
+ projectId: noProject ? NO_PROJECT_PLACEHOLDER : ctx.projectId,
161
+ middleware: noProject ? [dropProjectHeaders] : [],
142
162
  accountId: ctx.accountId,
143
163
  apiKey: ctx.apiKey,
144
164
  bearerToken: ctx.bearerToken,
@@ -1,19 +1,43 @@
1
- import { Args } from '@oclif/core';
1
+ import { NorbixError } from '@norbix.ai/ts';
2
+ import { Args, Flags } from '@oclif/core';
2
3
  import { BaseCommand } from '../../base.js';
3
4
  export default class PushPreview extends BaseCommand {
4
5
  static description = `Render the title, body and subtitle behind a push preview link
5
6
 
6
7
  The hash is the opaque value the backend puts in a preview link — you cannot
7
- build it yourself, so copy it out of the link.`;
8
- static examples = ['<%= config.bin %> push preview 8f2a91c4...'];
8
+ build it yourself, so copy it out of the link.
9
+
10
+ The signed link is the key: no login and no project are needed. When you are
11
+ logged in, your session is still sent.`;
12
+ static examples = [
13
+ '<%= config.bin %> push preview 8f2a91c4...',
14
+ '<%= config.bin %> push preview --hash 8f2a91c4... --region nb-eu-germany',
15
+ ];
9
16
  static args = {
10
- hash: Args.string({ required: true, description: 'Preview hash from the preview link' }),
17
+ hash: Args.string({ required: false, description: 'Preview hash from the preview link' }),
18
+ };
19
+ static flags = {
20
+ hash: Flags.string({ description: 'Preview hash from the preview link (same as the argument)' }),
11
21
  };
12
22
  async run() {
13
23
  const { args, flags } = await this.parse(PushPreview);
14
- const client = this.client(flags);
15
- const res = await client.hub.notifications.previewPushNotification({ hash: args.hash });
24
+ const hash = flags.hash ?? args.hash;
25
+ if (!hash) {
26
+ this.error('Pass the preview hash: `norbix push preview <hash>` or `--hash <hash>`.');
27
+ }
28
+ // The signed link opens without sign-in, so neither a login nor a project
29
+ // is forced here. A login, when there is one, is still used.
30
+ const client = this.client(flags, { requireAuth: false, requireProject: false });
31
+ const res = await client.hub.notifications.previewPushNotification({ hash });
16
32
  this.print(res);
17
33
  return res;
18
34
  }
35
+ async catch(error) {
36
+ // A 401 here means the link is bad or expired, not that a session ran out.
37
+ if (error instanceof NorbixError && error.status === 401) {
38
+ const { code } = error;
39
+ return this.error(`${code ? `${code}: ` : ''}${error.message}\nThe preview link is invalid or has expired. Ask for a new link.`);
40
+ }
41
+ return super.catch(error);
42
+ }
19
43
  }