@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 +67 -0
- package/bin/sure-cli.js +202 -0
- package/lib/api.js +68 -0
- package/lib/config.js +51 -0
- package/package.json +34 -0
- package/templates/customer.json +24 -0
- package/templates/status-change.json +3 -0
- package/templates/workflow-start.json +5 -0
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.
|
package/bin/sure-cli.js
ADDED
|
@@ -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
|
+
}
|