@surelycrm/cli 1.0.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 ADDED
@@ -0,0 +1,67 @@
1
+ # @surelycrm/cli
2
+
3
+ A command-line interface for the SurelyCrm API.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 18 or later
8
+ - A SurelyCrm API key (from **Settings > Application Settings**)
9
+
10
+ ## Installation
11
+
12
+ Install globally from npm:
13
+
14
+ ```bash
15
+ npm install -g @surelycrm/cli
16
+ ```
17
+
18
+ Or run without installing:
19
+
20
+ ```bash
21
+ npx @surelycrm/cli --help
22
+ ```
23
+
24
+ To develop locally, clone the repo and link the package:
25
+
26
+ ```bash
27
+ cd Sure.Cli
28
+ npm install
29
+ npm link
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ### Initialise
35
+
36
+ ```bash
37
+ sure-cli init --url https://app.surelycrm.co.uk/ --api-key {your-api-key}
38
+ ```
39
+
40
+ Configuration is stored in `~/.surecli/config.json`.
41
+
42
+ ### Commands
43
+
44
+ | Command | Description |
45
+ |---------|-------------|
46
+ | `sure-cli config` | Show stored configuration |
47
+ | `sure-cli create-customer` | Create a new customer |
48
+ | `sure-cli get-customer --id {guid}` | Get a customer by GUID |
49
+ | `sure-cli get-customer-by-reference --reference {ref}` | Get a customer by reference number |
50
+ | `sure-cli update-customer --id {guid}` | Update an existing customer |
51
+ | `sure-cli change-status --customer-id {guid} --status-id {guid}` | Change a customer's status |
52
+ | `sure-cli start-workflow --customer-id {guid} --workflow-name {name}` | Start a workflow for a customer |
53
+ | `sure-cli get-customers-by-age --age {age} --comparison {comparison}` | Filter customers by age |
54
+
55
+ Use `--help` on any command for details.
56
+
57
+ ### Templates
58
+
59
+ JSON templates for complex payloads are in the `templates/` directory:
60
+
61
+ - `customer.json` — customer create/update payload
62
+ - `status-change.json` — status change payload
63
+ - `workflow-start.json` — workflow start payload
64
+
65
+ ## Documentation
66
+
67
+ See `guid/cli.html` for the full CLI reference guide.
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { loadConfig, saveConfig, requireConfig } = require('../lib/config');
7
+ const { request, handleResponse } = require('../lib/api');
8
+
9
+ const program = new Command();
10
+ program.name('sure-cli').description('CLI wrapper for the SurelyCrm API').version('1.0.0');
11
+
12
+ const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
13
+
14
+ function loadTemplate(name) {
15
+ const filePath = path.join(TEMPLATES_DIR, `${name}.json`);
16
+ if (!fs.existsSync(filePath)) {
17
+ console.error(`Template not found: ${filePath}`);
18
+ process.exit(1);
19
+ }
20
+ try {
21
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
22
+ } catch (err) {
23
+ console.error(`Failed to parse template ${filePath}: ${err.message}`);
24
+ process.exit(1);
25
+ }
26
+ }
27
+
28
+ function parseDataInput(options) {
29
+ if (options.data) {
30
+ try {
31
+ return JSON.parse(options.data);
32
+ } catch (err) {
33
+ console.error(`Failed to parse inline JSON: ${err.message}`);
34
+ process.exit(1);
35
+ }
36
+ }
37
+ if (options.template) {
38
+ try {
39
+ return JSON.parse(fs.readFileSync(options.template, 'utf8'));
40
+ } catch (err) {
41
+ console.error(`Failed to read template file: ${err.message}`);
42
+ process.exit(1);
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+
48
+ function getRequiredOption(options, name) {
49
+ const value = options[name];
50
+ if (!value) {
51
+ console.error(`Missing required option: --${name}`);
52
+ process.exit(1);
53
+ }
54
+ return value;
55
+ }
56
+
57
+ function run(action) {
58
+ return async (...args) => {
59
+ try {
60
+ await action(...args);
61
+ } catch (err) {
62
+ if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT') {
63
+ console.error(`Unable to connect to the API: ${err.message || err.code}`);
64
+ } else if (err.message) {
65
+ console.error(err.message);
66
+ } else {
67
+ console.error(err);
68
+ }
69
+ process.exit(1);
70
+ }
71
+ };
72
+ }
73
+
74
+ program
75
+ .command('init')
76
+ .description('Initialise the CLI with the API base URL and API key')
77
+ .requiredOption('--url <url>', 'Base URL of the SurelyCrm API (e.g. https://app.surelycrm.co.uk/)')
78
+ .requiredOption('--api-key <key>', 'API key GUID')
79
+ .action((options) => {
80
+ const baseUrl = options.url.endsWith('/') ? options.url : `${options.url}/`;
81
+ saveConfig({ baseUrl, apiKey: options.apiKey });
82
+ console.log('Configuration saved.');
83
+ });
84
+
85
+ program
86
+ .command('config')
87
+ .description('Show the current configuration')
88
+ .action(() => {
89
+ const config = requireConfig();
90
+ console.log(JSON.stringify(config, null, 2));
91
+ });
92
+
93
+ program
94
+ .command('create-customer')
95
+ .description('Create a new customer')
96
+ .option('--template <file>', 'Path to a JSON customer template')
97
+ .option('--data <json>', 'Inline JSON customer payload')
98
+ .action(run(async (options) => {
99
+ const config = requireConfig();
100
+ let body = parseDataInput(options);
101
+ if (!body) {
102
+ body = loadTemplate('customer');
103
+ }
104
+ const response = await request(config.baseUrl, config.apiKey, 'POST', 'Api/Create', body);
105
+ handleResponse(response);
106
+ }));
107
+
108
+ program
109
+ .command('get-customer')
110
+ .description('Get a customer by GUID')
111
+ .requiredOption('--id <guid>', 'Customer GUID')
112
+ .action(run(async (options) => {
113
+ const config = requireConfig();
114
+ const id = getRequiredOption(options, 'id');
115
+ const response = await request(config.baseUrl, config.apiKey, 'GET', `Api/customer/${id}`);
116
+ handleResponse(response);
117
+ }));
118
+
119
+ program
120
+ .command('get-customer-by-reference')
121
+ .description('Get a customer by reference number')
122
+ .requiredOption('--reference <reference>', 'Customer reference number')
123
+ .action(run(async (options) => {
124
+ const config = requireConfig();
125
+ const reference = getRequiredOption(options, 'reference');
126
+ const response = await request(config.baseUrl, config.apiKey, 'GET', `Api/customer/reference/${encodeURIComponent(reference)}`);
127
+ handleResponse(response);
128
+ }));
129
+
130
+ program
131
+ .command('update-customer')
132
+ .description('Update an existing customer')
133
+ .requiredOption('--id <guid>', 'Customer GUID')
134
+ .option('--template <file>', 'Path to a JSON customer template')
135
+ .option('--data <json>', 'Inline JSON customer payload')
136
+ .action(run(async (options) => {
137
+ const config = requireConfig();
138
+ const id = getRequiredOption(options, 'id');
139
+ let body = parseDataInput(options);
140
+ if (!body) {
141
+ body = loadTemplate('customer');
142
+ }
143
+ body.id = id;
144
+ const response = await request(config.baseUrl, config.apiKey, 'PUT', `Api/customer/${id}`, body);
145
+ handleResponse(response);
146
+ }));
147
+
148
+ program
149
+ .command('change-status')
150
+ .description('Change a customer account status')
151
+ .requiredOption('--customer-id <guid>', 'Customer GUID')
152
+ .requiredOption('--status-id <guid>', 'Status GUID')
153
+ .action(run(async (options) => {
154
+ const config = requireConfig();
155
+ const customerId = getRequiredOption(options, 'customerId');
156
+ const statusId = getRequiredOption(options, 'statusId');
157
+ const body = { statusId };
158
+ const response = await request(config.baseUrl, config.apiKey, 'POST', `Api/customer/${customerId}/status`, body);
159
+ handleResponse(response);
160
+ }));
161
+
162
+ program
163
+ .command('start-workflow')
164
+ .description('Start a workflow for a customer')
165
+ .requiredOption('--customer-id <guid>', 'Customer GUID')
166
+ .option('--workflow-id <guid>', 'Workflow GUID')
167
+ .option('--workflow-name <name>', 'Workflow name')
168
+ .option('--context-data <json>', 'JSON context data for the workflow instance', '{}')
169
+ .action(run(async (options) => {
170
+ const config = requireConfig();
171
+ const customerId = getRequiredOption(options, 'customerId');
172
+
173
+ if (!options.workflowId && !options.workflowName) {
174
+ console.error('Either --workflow-id or --workflow-name is required');
175
+ process.exit(1);
176
+ }
177
+
178
+ const body = {
179
+ customerId,
180
+ contextData: options.contextData
181
+ };
182
+ if (options.workflowId) body.workflowId = options.workflowId;
183
+ if (options.workflowName) body.workflowName = options.workflowName;
184
+
185
+ const response = await request(config.baseUrl, config.apiKey, 'POST', `Api/customer/${customerId}/workflow`, body);
186
+ handleResponse(response);
187
+ }));
188
+
189
+ program
190
+ .command('get-customers-by-age')
191
+ .description('Get customers filtered by age')
192
+ .requiredOption('--age <age>', 'Age value', parseInt)
193
+ .option('--comparison <comparison>', 'Comparison operator (equal, greater, less, greaterthanorequal, lessthanorequal)', 'equal')
194
+ .action(run(async (options) => {
195
+ const config = requireConfig();
196
+ const age = getRequiredOption(options, 'age');
197
+ const comparison = options.comparison;
198
+ const response = await request(config.baseUrl, config.apiKey, 'GET', `Api/customers/byAge?age=${age}&comparison=${encodeURIComponent(comparison)}`);
199
+ handleResponse(response);
200
+ }));
201
+
202
+ program.parse();
package/lib/api.js ADDED
@@ -0,0 +1,68 @@
1
+ const http = require('http');
2
+ const https = require('https');
3
+ const { URL } = require('url');
4
+
5
+ function request(baseUrl, apiKey, method, path, body = null) {
6
+ return new Promise((resolve, reject) => {
7
+ const url = new URL(path, baseUrl);
8
+ const transport = url.protocol === 'https:' ? https : http;
9
+
10
+ const options = {
11
+ hostname: url.hostname,
12
+ port: url.port,
13
+ path: url.pathname + url.search,
14
+ method: method.toUpperCase(),
15
+ headers: {
16
+ 'apiKey': apiKey,
17
+ 'Accept': 'application/json'
18
+ }
19
+ };
20
+
21
+ let payload = null;
22
+ if (body) {
23
+ payload = JSON.stringify(body);
24
+ options.headers['Content-Type'] = 'application/json';
25
+ options.headers['Content-Length'] = Buffer.byteLength(payload);
26
+ }
27
+
28
+ const req = transport.request(options, (res) => {
29
+ let data = '';
30
+ res.on('data', (chunk) => {
31
+ data += chunk;
32
+ });
33
+ res.on('end', () => {
34
+ try {
35
+ const parsed = data ? JSON.parse(data) : null;
36
+ resolve({ status: res.statusCode, data: parsed, raw: data });
37
+ } catch {
38
+ resolve({ status: res.statusCode, data: data, raw: data });
39
+ }
40
+ });
41
+ });
42
+
43
+ req.on('error', (err) => {
44
+ reject(err);
45
+ });
46
+
47
+ if (payload) {
48
+ req.write(payload);
49
+ }
50
+ req.end();
51
+ });
52
+ }
53
+
54
+ function handleResponse(response) {
55
+ const output = JSON.stringify(response.data, null, 2);
56
+ if (response.status >= 200 && response.status < 300) {
57
+ console.log(output);
58
+ } else {
59
+ console.error(`HTTP ${response.status}`);
60
+ console.error(output);
61
+ process.exit(1);
62
+ }
63
+ }
64
+
65
+ module.exports = {
66
+ request,
67
+ handleResponse
68
+ };
package/lib/config.js ADDED
@@ -0,0 +1,51 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), '.surecli');
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
+
8
+ function ensureConfigDir() {
9
+ if (!fs.existsSync(CONFIG_DIR)) {
10
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
11
+ }
12
+ }
13
+
14
+ function loadConfig() {
15
+ ensureConfigDir();
16
+ if (!fs.existsSync(CONFIG_FILE)) {
17
+ return null;
18
+ }
19
+ try {
20
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
21
+ } catch (err) {
22
+ console.error('Error reading config file:', err.message);
23
+ process.exit(1);
24
+ }
25
+ }
26
+
27
+ function saveConfig(config) {
28
+ ensureConfigDir();
29
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
30
+ }
31
+
32
+ function requireConfig() {
33
+ const config = loadConfig();
34
+ if (!config) {
35
+ console.error('Configuration not found. Run: sure-cli init');
36
+ process.exit(1);
37
+ }
38
+ if (!config.baseUrl || !config.apiKey) {
39
+ console.error('Configuration is incomplete. Run: sure-cli init');
40
+ process.exit(1);
41
+ }
42
+ return config;
43
+ }
44
+
45
+ module.exports = {
46
+ CONFIG_DIR,
47
+ CONFIG_FILE,
48
+ loadConfig,
49
+ saveConfig,
50
+ requireConfig
51
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@surelycrm/cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI wrapper for the SurelyCrm API",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "sure-cli": "./bin/sure-cli.js"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "lib/",
15
+ "templates/",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "test": "node bin/sure-cli.js --help"
20
+ },
21
+ "keywords": [
22
+ "surelycrm",
23
+ "cli",
24
+ "api"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "commander": "^12.1.0"
30
+ },
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ }
34
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "title": "Mr",
3
+ "firstname": "John",
4
+ "surname": "Davies",
5
+ "referenceNumber": "",
6
+ "address1": "123 High Street",
7
+ "address2": "",
8
+ "address3": "",
9
+ "address4": "",
10
+ "town": "Manchester",
11
+ "city": "Greater Manchester",
12
+ "postcode": "M1 1AA",
13
+ "homePhone": "",
14
+ "mobilePhone": "07700900123",
15
+ "emailAddress": "john.davies@example.com",
16
+ "notificationsEnabled": true,
17
+ "importantDate": null,
18
+ "dateOfBirth": null,
19
+ "website": "",
20
+ "statusId": null,
21
+ "ownerId": null,
22
+ "referredBy": "",
23
+ "customFields": {}
24
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "statusId": "00000000-0000-0000-0000-000000000000"
3
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "workflowId": "00000000-0000-0000-0000-000000000000",
3
+ "workflowName": "",
4
+ "contextData": "{}"
5
+ }