@norbix.ai/cli 0.3.0 → 1.1.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
@@ -48,7 +48,8 @@ norbix scheduler list
48
48
  | `norbix apikeys list/regenerate` | Show or regenerate project API keys |
49
49
  | `norbix webhooks show/secret/enable/disable/remove` | Inspect the webhook integration |
50
50
  | `norbix email templates/template/clone/archive/unarchive/delete/campaigns/campaign/stop` | Email templates and campaigns |
51
- | `norbix push ...` / `norbix sms ...` | Push and SMS — same commands as email |
51
+ | `norbix push ...` | Push module, integrations, devices, templates, campaigns — every push endpoint, see [docs/push.md](docs/push.md) |
52
+ | `norbix sms ...` | SMS — same commands as email |
52
53
  | `norbix account profile/status/usage/projects/team/regions/billing-portal` | Account-level info |
53
54
  | `norbix payments integrations/triggers/trigger/enable/disable` | Payment integrations and triggers |
54
55
  | `norbix integrations <module>` | List integrations of any module |
@@ -125,6 +126,29 @@ pipe into `jq`:
125
126
  norbix db find orders --json | jq '.list.items[] | ._id'
126
127
  ```
127
128
 
129
+ ## Errors
130
+
131
+ A failed call prints one line: the error code, the message, and the HTTP
132
+ status.
133
+
134
+ ```sh
135
+ $ norbix files info nbin_1 a/b.txt
136
+ › Error: CM-ERRORS-FILES-016: File not found: "a/b.txt" does not exist in
137
+ › Local (nbin_1). (HTTP 404)
138
+ ```
139
+
140
+ The code and the message are the gateway's own — they come out of
141
+ `responseStatus.errors[]`, where the gateway puts them. Before this, every
142
+ failure read "Request failed with status 404" with no code.
143
+
144
+ Exit codes do not change: `1` for a failed call, `2` for a check that ran and
145
+ came back negative (`norbix files integrations test`).
146
+
147
+ **A refusal the gateway answers with HTTP 200** and
148
+ `responseStatus.isSuccess = false` now exits non-zero as well, wherever it did
149
+ not before — `norbix files publish` / `unpublish` were the two commands that
150
+ used to treat it as a success.
151
+
128
152
  ## Development
129
153
 
130
154
  ```sh
package/dist/base.js CHANGED
@@ -157,12 +157,23 @@ export class BaseCommand extends Command {
157
157
  this.log(typeof data === 'string' ? data : JSON.stringify(data, null, 2));
158
158
  }
159
159
  }
160
+ /**
161
+ * One line for a failed call: `<errorCode>: <message> (HTTP <status>)`.
162
+ *
163
+ * The code comes first because it is the part a reader can search for and a
164
+ * script can match on. It is the gateway's own code — the SDK reads it out
165
+ * of `responseStatus.errors[]`, where the gateway puts it (10b-files slice
166
+ * ERRORS, issue #66). The exit codes do not change.
167
+ */
160
168
  async catch(error) {
161
169
  if (error instanceof NorbixError) {
162
- const status = error.status;
163
- const suffix = status ? ` (HTTP ${status})` : '';
170
+ const { code, status } = error;
171
+ const prefix = code ? `${code}: ` : '';
172
+ // The SDK's last-resort message already ends in "(HTTP <status>)" —
173
+ // do not say it twice.
174
+ const suffix = status && !error.message.includes(`(HTTP ${status})`) ? ` (HTTP ${status})` : '';
164
175
  const hint = status === 401 ? '\nYour session may have expired. Run `norbix login` again.' : '';
165
- return this.error(`${error.message}${suffix}${hint}`);
176
+ return this.error(`${prefix}${error.message}${suffix}${hint}`);
166
177
  }
167
178
  return super.catch(error);
168
179
  }
@@ -0,0 +1,39 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base.js';
3
+ export default class PushCampaignBatch extends BaseCommand {
4
+ static description = `List the notifications in one batch — or show one of them
5
+
6
+ Give a notification ID as the third argument to see just that notification.`;
7
+ static examples = [
8
+ '<%= config.bin %> push campaign batch 66b2f0a1... b7c3...',
9
+ '<%= config.bin %> push campaign batch 66b2f0a1... b7c3... n9d4...',
10
+ ];
11
+ static args = {
12
+ id: Args.string({ required: true, description: 'Campaign ID' }),
13
+ batchId: Args.string({ required: true, description: 'Batch ID (from `push campaign batches`)' }),
14
+ notificationId: Args.string({ description: 'Notification ID' }),
15
+ };
16
+ static flags = {
17
+ 'page-size': Flags.integer({ description: 'Items per page' }),
18
+ after: Flags.string({ description: 'Cursor from the previous page' }),
19
+ };
20
+ async run() {
21
+ const { args, flags } = await this.parse(PushCampaignBatch);
22
+ const client = this.client(flags);
23
+ const n = client.hub.notifications;
24
+ const res = args.notificationId
25
+ ? await n.getPushCampaignBatchNotification({
26
+ id: args.id,
27
+ batchId: args.batchId,
28
+ notificationId: args.notificationId,
29
+ })
30
+ : await n.getPushCampaignBatchNotifications({
31
+ id: args.id,
32
+ batchId: args.batchId,
33
+ pageSize: flags['page-size'],
34
+ startingAfter: flags.after,
35
+ });
36
+ this.print(res);
37
+ return res;
38
+ }
39
+ }
@@ -0,0 +1,24 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base.js';
3
+ export default class PushCampaignBatches extends BaseCommand {
4
+ static description = 'List the send batches of a push campaign';
5
+ static examples = ['<%= config.bin %> push campaign batches 66b2f0a1...'];
6
+ static args = {
7
+ id: Args.string({ required: true, description: 'Campaign ID' }),
8
+ };
9
+ static flags = {
10
+ 'page-size': Flags.integer({ description: 'Items per page' }),
11
+ after: Flags.string({ description: 'Cursor from the previous page' }),
12
+ };
13
+ async run() {
14
+ const { args, flags } = await this.parse(PushCampaignBatches);
15
+ const client = this.client(flags);
16
+ const res = await client.hub.notifications.getPushCampaignBatches({
17
+ id: args.id,
18
+ pageSize: flags['page-size'],
19
+ startingAfter: flags.after,
20
+ });
21
+ this.print(res);
22
+ return res;
23
+ }
24
+ }
@@ -0,0 +1,102 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base.js';
3
+ import { parseTokens, readJsonObject } from '../../../lib/push.js';
4
+ /** --audience value → the `source` discriminator the server routes the body by. */
5
+ const AUDIENCES = {
6
+ 'all-users': 'allUsers',
7
+ users: 'specifiedUsers',
8
+ 'account-users': 'accountUsers',
9
+ collection: 'collection',
10
+ devices: 'devices',
11
+ };
12
+ export default class PushCampaignCreate extends BaseCommand {
13
+ static description = `Create a push campaign
14
+
15
+ --audience picks who receives it:
16
+ all-users every user; narrow with --role / --tag
17
+ users the users named with --user
18
+ account-users the account users named with --user
19
+ collection the users referenced by --field in the --schema collection
20
+ devices raw device tokens, --device <token>:<family> (Ios, Android, Chrome, Safari, Expo)
21
+
22
+ Without --at the campaign is sent right away. Anything the flags do not cover
23
+ goes in --config as a JSON object and is merged into the campaign.`;
24
+ static examples = [
25
+ '<%= config.bin %> push campaign create --template 66b2f0a1... --audience all-users --tag beta',
26
+ '<%= config.bin %> push campaign create --template 66b2f0a1... --audience users --user usr_1 --user usr_2',
27
+ '<%= config.bin %> push campaign create --template 66b2f0a1... --audience devices --device dGVzdA==:Ios',
28
+ ];
29
+ static flags = {
30
+ template: Flags.string({ required: true, description: 'Template ID' }),
31
+ audience: Flags.string({ required: true, description: 'Who receives it', options: Object.keys(AUDIENCES) }),
32
+ user: Flags.string({ description: 'User ID (users / account-users; repeat for several)', multiple: true }),
33
+ role: Flags.string({ description: 'Role name (all-users / collection; repeat for several)', multiple: true }),
34
+ tag: Flags.string({ description: 'User tag (all-users; repeat for several)', multiple: true }),
35
+ schema: Flags.string({ description: 'Collection name (collection)' }),
36
+ field: Flags.string({ description: 'Field that holds the recipient (collection; repeat for several)', multiple: true }),
37
+ 'field-type': Flags.string({ description: 'What --field holds (collection)', options: ['User', 'Email'] }),
38
+ device: Flags.string({ description: 'Device as <token>:<family> (devices; repeat for several)', multiple: true }),
39
+ integration: Flags.string({ description: 'Push integration ID (defaults to the project default)' }),
40
+ language: Flags.string({ description: 'Template language to send' }),
41
+ notes: Flags.string({ description: 'Internal notes' }),
42
+ at: Flags.string({ description: 'Send later: ISO 8601 date-time or Unix seconds' }),
43
+ token: Flags.string({ description: 'Token value as key=value (repeat for several)', multiple: true }),
44
+ 'database-integration': Flags.string({ description: 'Database integration ID (defaults to the project default)' }),
45
+ config: Flags.string({ description: 'Extra campaign fields as a JSON object, @file or -' }),
46
+ };
47
+ async run() {
48
+ const { flags } = await this.parse(PushCampaignCreate);
49
+ const client = this.client(flags);
50
+ const extra = flags.config ? await readJsonObject(flags.config, 'config') : {};
51
+ const campaign = {
52
+ ...extra,
53
+ source: AUDIENCES[flags.audience],
54
+ templateId: flags.template,
55
+ integrationId: flags.integration,
56
+ language: flags.language,
57
+ notes: flags.notes,
58
+ campaignTime: flags.at ? toUnixSeconds(flags.at) : undefined,
59
+ mappedTokens: parseTokens(flags.token),
60
+ ...this.audienceFields(flags),
61
+ };
62
+ const res = await client.hub.notifications.createPushCampaign({
63
+ campaign,
64
+ databaseIntegrationId: flags['database-integration'],
65
+ });
66
+ this.print(res);
67
+ return res;
68
+ }
69
+ audienceFields(flags) {
70
+ switch (flags.audience) {
71
+ case 'all-users':
72
+ return { rolesNames: flags.role, userTags: flags.tag };
73
+ case 'users':
74
+ case 'account-users':
75
+ if (!flags.user?.length)
76
+ this.error(`--audience ${flags.audience} needs at least one --user`);
77
+ return { userRecipients: flags.user };
78
+ case 'collection':
79
+ if (!flags.schema || !flags.field?.length)
80
+ this.error('--audience collection needs --schema and --field');
81
+ return { schemaName: flags.schema, fields: flags.field, fieldType: flags['field-type'], roleNames: flags.role };
82
+ default:
83
+ if (!flags.device?.length)
84
+ this.error('--audience devices needs at least one --device <token>:<family>');
85
+ return { devices: flags.device.map((d) => this.device(d)) };
86
+ }
87
+ }
88
+ device(value) {
89
+ const at = value.lastIndexOf(':');
90
+ if (at < 1)
91
+ this.error(`--device must look like <token>:<family>, got "${value}"`);
92
+ return { token: value.slice(0, at), deliveryFamily: value.slice(at + 1) };
93
+ }
94
+ }
95
+ function toUnixSeconds(value) {
96
+ if (/^\d+$/.test(value))
97
+ return Number(value);
98
+ const ms = Date.parse(value);
99
+ if (Number.isNaN(ms))
100
+ throw new Error(`--at is not a date: "${value}"`);
101
+ return Math.floor(ms / 1000);
102
+ }
@@ -0,0 +1,27 @@
1
+ import { confirm } from '@inquirer/prompts';
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../../base.js';
4
+ export default class PushCampaignDelete extends BaseCommand {
5
+ static description = 'Delete a push campaign';
6
+ static examples = ['<%= config.bin %> push campaign delete 66b2f0a1... --yes'];
7
+ static args = {
8
+ id: Args.string({ required: true, description: 'Campaign ID' }),
9
+ };
10
+ static flags = {
11
+ yes: Flags.boolean({ char: 'y', description: 'Skip the confirmation prompt', default: false }),
12
+ };
13
+ async run() {
14
+ const { args, flags } = await this.parse(PushCampaignDelete);
15
+ const client = this.client(flags);
16
+ if (!flags.yes && process.stdout.isTTY) {
17
+ const ok = await confirm({ message: `Delete push campaign ${args.id}?`, default: false });
18
+ if (!ok)
19
+ return this.print('Cancelled.');
20
+ }
21
+ // The route is `/campaigns/{Id}` but the generated request type is empty,
22
+ // so the id goes in as `Id` — the transport fills tokens by exact name.
23
+ const res = await client.hub.notifications.deletePushCampaign({ Id: args.id });
24
+ this.print(`Campaign ${args.id} deleted.`);
25
+ return res;
26
+ }
27
+ }
@@ -0,0 +1,28 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base.js';
3
+ export default class PushCampaignMessage extends BaseCommand {
4
+ static description = 'Show one push message a campaign sent';
5
+ static examples = ['<%= config.bin %> push campaign message 66b2f0a1... n9d4... --batch b7c3...'];
6
+ static args = {
7
+ id: Args.string({ required: true, description: 'Campaign ID' }),
8
+ messageId: Args.string({ required: true, description: 'Message (notification) ID' }),
9
+ };
10
+ static flags = {
11
+ batch: Flags.string({ required: true, description: 'Batch the message belongs to' }),
12
+ };
13
+ async run() {
14
+ const { args, flags } = await this.parse(PushCampaignMessage);
15
+ const client = this.client(flags);
16
+ // The gateway route now names its token `{notificationId}`, and the
17
+ // regenerated request type has the matching field — so the workaround
18
+ // this command used to carry (an extra `id` plus a cast, because the
19
+ // token and the field had different names) is gone.
20
+ const res = await client.hub.notifications.getPushCampaignMessage({
21
+ campaignId: args.id,
22
+ notificationId: args.messageId,
23
+ campaignBatchId: flags.batch,
24
+ });
25
+ this.print(res);
26
+ return res;
27
+ }
28
+ }
@@ -0,0 +1,26 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base.js';
3
+ export default class PushCampaignMessages extends BaseCommand {
4
+ static description = 'List the push messages a campaign sent';
5
+ static examples = ['<%= config.bin %> push campaign messages 66b2f0a1... --batch b7c3...'];
6
+ static args = {
7
+ id: Args.string({ required: true, description: 'Campaign ID' }),
8
+ };
9
+ static flags = {
10
+ batch: Flags.string({ description: 'Only this batch (from `push campaign batches`)' }),
11
+ 'page-size': Flags.integer({ description: 'Items per page' }),
12
+ after: Flags.string({ description: 'Cursor from the previous page' }),
13
+ };
14
+ async run() {
15
+ const { args, flags } = await this.parse(PushCampaignMessages);
16
+ const client = this.client(flags);
17
+ const res = await client.hub.notifications.getPushCampaignMessages({
18
+ campaignId: args.id,
19
+ campaignBatchId: flags.batch,
20
+ pageSize: flags['page-size'],
21
+ startingAfter: flags.after,
22
+ });
23
+ this.print(res);
24
+ return res;
25
+ }
26
+ }
@@ -0,0 +1,16 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../../base.js';
3
+ export default class PushDevice extends BaseCommand {
4
+ static description = 'Show one registered push device, with the user it belongs to';
5
+ static examples = ['<%= config.bin %> push device pnd_123'];
6
+ static args = {
7
+ id: Args.string({ required: true, description: 'Device ID' }),
8
+ };
9
+ async run() {
10
+ const { args, flags } = await this.parse(PushDevice);
11
+ const client = this.client(flags);
12
+ const res = await client.hub.notifications.getPushDevice({ id: args.id });
13
+ this.print(res);
14
+ return res;
15
+ }
16
+ }
@@ -0,0 +1,33 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base.js';
3
+ export default class PushDevices extends BaseCommand {
4
+ static description = `List the devices registered for push
5
+
6
+ Narrow the list with --user, --token or --platform. Devices are stored
7
+ inside their user, so a page is a page of users carrying every matching
8
+ device they hold — follow hasMore rather than stopping at a short page.`;
9
+ static examples = [
10
+ '<%= config.bin %> push devices',
11
+ '<%= config.bin %> push devices --platform ios',
12
+ '<%= config.bin %> push devices --user usr_123',
13
+ ];
14
+ static flags = {
15
+ user: Flags.string({ description: 'Only the devices of this user' }),
16
+ token: Flags.string({ description: 'Only the device registered with this provider token' }),
17
+ platform: Flags.string({
18
+ description: 'Only devices of this platform',
19
+ options: ['ios', 'android', 'chrome', 'safari', 'expo'],
20
+ }),
21
+ };
22
+ async run() {
23
+ const { flags } = await this.parse(PushDevices);
24
+ const client = this.client(flags);
25
+ const res = await client.hub.notifications.getPushDevices({
26
+ userId: flags.user,
27
+ deviceKey: flags.token,
28
+ platform: flags.platform,
29
+ });
30
+ this.print(res);
31
+ return res;
32
+ }
33
+ }
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ export default class PushDisableDependencies extends BaseCommand {
3
+ static description = 'List what still depends on push — check this before `push disable`';
4
+ static examples = ['<%= config.bin %> push disable-dependencies'];
5
+ async run() {
6
+ const { flags } = await this.parse(PushDisableDependencies);
7
+ const client = this.client(flags);
8
+ const res = await client.hub.notifications.getPushDisableDependencies({});
9
+ this.print(res);
10
+ return res;
11
+ }
12
+ }
@@ -0,0 +1,24 @@
1
+ import { confirm } from '@inquirer/prompts';
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../base.js';
4
+ export default class PushDisable extends BaseCommand {
5
+ static description = `Turn off the push module for the project
6
+
7
+ Run \`push disable-dependencies\` first to see what still depends on push.`;
8
+ static examples = ['<%= config.bin %> push disable --yes'];
9
+ static flags = {
10
+ yes: Flags.boolean({ char: 'y', description: 'Skip the confirmation prompt', default: false }),
11
+ };
12
+ async run() {
13
+ const { flags } = await this.parse(PushDisable);
14
+ const client = this.client(flags);
15
+ if (!flags.yes && process.stdout.isTTY) {
16
+ const ok = await confirm({ message: 'Turn off push for this project?', default: false });
17
+ if (!ok)
18
+ return this.print('Cancelled.');
19
+ }
20
+ const res = await client.hub.notifications.disablePush({});
21
+ this.print('Push disabled.');
22
+ return res;
23
+ }
24
+ }
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ export default class PushEnable extends BaseCommand {
3
+ static description = 'Turn on the push module for the project';
4
+ static examples = ['<%= config.bin %> push enable'];
5
+ async run() {
6
+ const { flags } = await this.parse(PushEnable);
7
+ const client = this.client(flags);
8
+ const res = await client.hub.notifications.enablePush({});
9
+ this.print('Push enabled.');
10
+ return res;
11
+ }
12
+ }
@@ -0,0 +1,34 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base.js';
3
+ export default class PushIntegrationAppRequest extends BaseCommand {
4
+ static description = `Register a Norbix app push integration request (managed service)
5
+
6
+ This is one step of the managed-app pairing flow: the Norbix mobile app shows
7
+ a request ID, a PIN and a public key, and this call registers them for the
8
+ account. It is account-scoped, so an account ID must be configured or passed
9
+ with --account.`;
10
+ static examples = [
11
+ '<%= config.bin %> push integration app-request --user usr_1 --request-id req_1 --pin 123456 --valid-till 2026-09-20T12:00:00Z --public-key MIIB...',
12
+ ];
13
+ static flags = {
14
+ user: Flags.string({ required: true, description: 'User ID the app is paired to' }),
15
+ 'request-id': Flags.string({ required: true, description: 'Request ID shown by the app' }),
16
+ pin: Flags.integer({ required: true, description: 'PIN shown by the app' }),
17
+ 'valid-till': Flags.string({ required: true, description: 'When the request expires (ISO 8601)' }),
18
+ 'public-key': Flags.string({ required: true, description: "The app's public key" }),
19
+ };
20
+ async run() {
21
+ const { flags } = await this.parse(PushIntegrationAppRequest);
22
+ const client = this.client(flags);
23
+ const res = await client.hub.notifications.registerCodeMashAppPushIntegration({
24
+ accountId: this.resolveContext(flags).accountId,
25
+ userId: flags.user,
26
+ requestId: flags['request-id'],
27
+ pin: flags.pin,
28
+ validTill: flags['valid-till'],
29
+ publicKey: flags['public-key'],
30
+ });
31
+ this.print('App push integration request registered.');
32
+ return res;
33
+ }
34
+ }
@@ -0,0 +1,19 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base.js';
3
+ export default class PushIntegrationConfirmDelivery extends BaseCommand {
4
+ static description = `Confirm that a test push reached a real person
5
+
6
+ After \`push integration test\`, run this once you have seen the push on the
7
+ device. It marks the integration as proven to deliver.`;
8
+ static examples = ['<%= config.bin %> push integration confirm-delivery 66b2f0a1...'];
9
+ static args = {
10
+ id: Args.string({ required: true, description: 'Integration ID' }),
11
+ };
12
+ async run() {
13
+ const { args, flags } = await this.parse(PushIntegrationConfirmDelivery);
14
+ const client = this.client(flags);
15
+ const res = await client.hub.notifications.confirmPushIntegrationHumanDelivery({ integrationId: args.id });
16
+ this.print(`Delivery confirmed for integration ${args.id}.`);
17
+ return res;
18
+ }
19
+ }
@@ -0,0 +1,41 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base.js';
3
+ import { readJsonObject } from '../../../lib/push.js';
4
+ /** The provider names the server's integration converter accepts. */
5
+ const PROVIDERS = ['Fake', 'AppleApns', 'AndroidFirebase', 'SafariPush', 'ChromeWeb', 'FirefoxWeb', 'EdgeWeb', 'ChromePush'];
6
+ export default class PushIntegrationSave extends BaseCommand {
7
+ static description = `Create or update a push integration
8
+
9
+ --provider picks the body shape. Fake needs nothing else: it accepts every send
10
+ and contacts no push service, so use it while you develop. The real providers
11
+ need their credentials — put the provider-specific fields in --config (inline
12
+ JSON, @file.json, or - for stdin). Pass --id to update an existing integration.`;
13
+ static examples = [
14
+ '<%= config.bin %> push integration save --provider Fake',
15
+ '<%= config.bin %> push integration save --provider AndroidFirebase --name Android --config @firebase.json',
16
+ ];
17
+ static flags = {
18
+ provider: Flags.string({ required: true, description: 'Push provider', options: PROVIDERS }),
19
+ name: Flags.string({ description: 'Integration name (Fake ignores it and uses its own)' }),
20
+ id: Flags.string({ description: 'Integration ID — set it to update instead of create' }),
21
+ disabled: Flags.boolean({ description: 'Save it turned off', default: false }),
22
+ config: Flags.string({ description: 'Provider-specific fields as a JSON object, @file or -' }),
23
+ };
24
+ async run() {
25
+ const { flags } = await this.parse(PushIntegrationSave);
26
+ const client = this.client(flags);
27
+ const extra = flags.config ? await readJsonObject(flags.config, 'config') : {};
28
+ const integration = {
29
+ ...extra,
30
+ provider: flags.provider,
31
+ integrationId: flags.id,
32
+ integrationName: flags.name ?? '',
33
+ isEnabled: !flags.disabled,
34
+ };
35
+ // The provider-specific fields are not on the generated base type, so the
36
+ // body is cast: the server picks the shape from `provider`.
37
+ const res = await client.hub.notifications.savePushIntegration({ integration });
38
+ this.print(res);
39
+ return res;
40
+ }
41
+ }
@@ -18,12 +18,10 @@ export default class PushStop extends BaseCommand {
18
18
  if (!ok)
19
19
  return this.print('Cancelled.');
20
20
  }
21
- const n = client.hub.notifications;
22
- if (!n.stopPushCampaign) {
23
- this.error('Your installed @norbix.ai/ts does not support stopping campaigns yet.\n' +
24
- `Update the SDK, or run: norbix api "/{version}/notifications/push/campaigns/${args.id}/stop" --hub --method POST`);
25
- }
26
- const res = await n.stopPushCampaign({ id: args.id });
21
+ // The route is `/campaigns/{Id}/stop` and the transport fills tokens by
22
+ // exact name, but the generated request type has no field at all — so the
23
+ // id goes in as `Id`. Sending `id` throws NORBIX_MISSING_PATH_PARAM.
24
+ const res = await client.hub.notifications.stopPushCampaign({ Id: args.id });
27
25
  this.print(`Campaign ${args.id} stopped.`);
28
26
  return res;
29
27
  }
@@ -0,0 +1,20 @@
1
+ import { BaseCommand } from '../../../base.js';
2
+ import { templateBody, templateFlags } from '../../../lib/push.js';
3
+ export default class PushTemplateCreate extends BaseCommand {
4
+ static description = `Create a push template
5
+
6
+ One language: --title and --body. Several languages: --translations with a
7
+ JSON array of {language, content: {title, body}}.`;
8
+ static examples = [
9
+ '<%= config.bin %> push template create --name Welcome --title "Hi @Model.Name" --body "Thanks for joining"',
10
+ '<%= config.bin %> push template create --name Welcome --translations @welcome.json',
11
+ ];
12
+ static flags = templateFlags;
13
+ async run() {
14
+ const { flags } = await this.parse(PushTemplateCreate);
15
+ const client = this.client(flags);
16
+ const res = await client.hub.notifications.createPushTemplate((await templateBody(flags)));
17
+ this.print(res);
18
+ return res;
19
+ }
20
+ }
@@ -0,0 +1,28 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base.js';
3
+ import { parseTokens } from '../../../lib/push.js';
4
+ export default class PushTemplateRender extends BaseCommand {
5
+ static description = `Render one push field (title, body or subtitle) with token values
6
+
7
+ Use it to check a Razor template before you save it. When the template needs a
8
+ token you did not pass, the answer lists the missing ones.`;
9
+ static examples = [
10
+ '<%= config.bin %> push template render --code "Hi @Model.Name" --token Name=Ada',
11
+ ];
12
+ static flags = {
13
+ code: Flags.string({ required: true, description: 'Razor source of the field' }),
14
+ token: Flags.string({ description: 'Token value as key=value (repeat for several)', multiple: true }),
15
+ preview: Flags.boolean({ description: 'Render for a preview (less strict)', default: false }),
16
+ };
17
+ async run() {
18
+ const { flags } = await this.parse(PushTemplateRender);
19
+ const client = this.client(flags);
20
+ const res = await client.hub.notifications.renderPush({
21
+ code: flags.code,
22
+ tokens: parseTokens(flags.token),
23
+ isForPreview: flags.preview,
24
+ });
25
+ this.print(res);
26
+ return res;
27
+ }
28
+ }
@@ -0,0 +1,16 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base.js';
3
+ export default class PushTemplateTokens extends BaseCommand {
4
+ static description = 'List the tokens a push template uses — the values a campaign must fill';
5
+ static examples = ['<%= config.bin %> push template tokens 66b2f0a1...'];
6
+ static args = {
7
+ id: Args.string({ required: true, description: 'Template ID' }),
8
+ };
9
+ async run() {
10
+ const { args, flags } = await this.parse(PushTemplateTokens);
11
+ const client = this.client(flags);
12
+ const res = await client.hub.notifications.getPushMessageContentTokens({ id: args.id });
13
+ this.print(res);
14
+ return res;
15
+ }
16
+ }
@@ -0,0 +1,25 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base.js';
3
+ import { templateBody, templateFlags } from '../../../lib/push.js';
4
+ export default class PushTemplateUpdate extends BaseCommand {
5
+ static description = `Replace a push template's name, channel, tags and content
6
+
7
+ The whole template is sent, so pass every language you want to keep.`;
8
+ static examples = [
9
+ '<%= config.bin %> push template update 66b2f0a1... --name Welcome --title "Hi" --body "Thanks for joining"',
10
+ ];
11
+ static args = {
12
+ id: Args.string({ required: true, description: 'Template ID' }),
13
+ };
14
+ static flags = templateFlags;
15
+ async run() {
16
+ const { args, flags } = await this.parse(PushTemplateUpdate);
17
+ const client = this.client(flags);
18
+ const res = await client.hub.notifications.updatePushTemplate({
19
+ ...(await templateBody(flags)),
20
+ viewId: args.id,
21
+ });
22
+ this.print(`Template ${args.id} updated.`);
23
+ return res;
24
+ }
25
+ }