@fleetkeep/cli 0.4.0 → 0.6.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/api-command.mjs +5 -2
- package/fleetkeep.mjs +98 -2
- package/operations.mjs +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,6 +5,73 @@ API. Friendly commands cover common vehicle and spreadsheet work. The allowliste
|
|
|
5
5
|
command covers equipment, maintenance, defects, walkarounds, files, reminders, contacts, bookings,
|
|
6
6
|
tolls, tracking, guidance and exports without opening arbitrary or unpublished paths.
|
|
7
7
|
|
|
8
|
+
## Shared jobs and running costs
|
|
9
|
+
|
|
10
|
+
Friendly commands return JSON, including missing-data and stale-tracking fields:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
fleetkeep costs list --from 2026-09-01 --to 2026-09-30
|
|
14
|
+
fleetkeep costs record --data-file expense.json
|
|
15
|
+
fleetkeep drivers activity --search JOB-42
|
|
16
|
+
fleetkeep tracking list
|
|
17
|
+
fleetkeep tracking route <vehicle-id> --period 7d
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Driver activity returns up to 100 journeys and 100 inspections from the company's
|
|
21
|
+
central record. Search filters journeys by job reference, driver or registration;
|
|
22
|
+
inspections remain the latest 100. The activity endpoint must be deployed before
|
|
23
|
+
using this command. Route history requires a supported connected tracker.
|
|
24
|
+
Expense files use the fields below; keep the same UUID and body when retrying.
|
|
25
|
+
|
|
26
|
+
`fleetkeep jobs list --search JOB-42` finds company jobs. Use
|
|
27
|
+
`fleetkeep jobs save JOB-42 --title "Depot installation"` to create or update a reference,
|
|
28
|
+
and `fleetkeep jobs close <job-id>` or `reopen <job-id>` to change its availability.
|
|
29
|
+
Closing a job preserves its journey history. These owner operations need an active Fleet plan.
|
|
30
|
+
|
|
31
|
+
`fleetkeep api call getRunningCosts --query from=2026-09-01 --query to=2026-09-30`
|
|
32
|
+
returns recorded GBP costs and missing-data flags. `recordFleetExpense` accepts a JSON body
|
|
33
|
+
with `id` (UUID), `vehicleId`, `incurredOn`, `category`, `amount` (GBP decimal string) and
|
|
34
|
+
`description`. Reuse the UUID and identical body on retries. Dates span up to one year.
|
|
35
|
+
Supported categories are fuel, insurance, repair, parking and other. Negative amounts record
|
|
36
|
+
refunds or corrections. Service costs and recorded GBP tolls are included separately, so do
|
|
37
|
+
not enter them again. Account JSON exports include jobs, journeys and additional costs.
|
|
38
|
+
|
|
39
|
+
Xero authorisation and bill review take place in the web portal. Saved SHVL grants and
|
|
40
|
+
credentials are not transferred between products or customer workspaces.
|
|
41
|
+
|
|
42
|
+
## Service intervals and equipment
|
|
43
|
+
|
|
44
|
+
Set one service interval in days, weeks or months. The vehicle command clears the
|
|
45
|
+
alternative unit when switching, and converts weeks to days:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
fleetkeep vehicles update <vehicle-id> --service-interval-weeks 6
|
|
49
|
+
fleetkeep vehicles update <vehicle-id> --service-interval-months 12
|
|
50
|
+
fleetkeep vehicles update <vehicle-id> --clear serviceIntervalDays,serviceIntervalMonths
|
|
51
|
+
fleetkeep api call getWalkaroundSummary
|
|
52
|
+
fleetkeep api call listVehicleEquipment --path id=<vehicle-id>
|
|
53
|
+
fleetkeep api call updateVehicleEquipment --path id=<vehicle-id> --data-file equipment.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The walkaround summary returns vehicles checked today in London time, not a
|
|
57
|
+
roadworthiness certificate. Equipment payloads support `presetKey`, provider,
|
|
58
|
+
reference, cover dates, reminders and up to eight `details` label/value pairs.
|
|
59
|
+
Use an `Arrangement` detail for the confirmed ownership type, such as `Hired`.
|
|
60
|
+
Omitting details preserves existing values; an empty array explicitly clears them.
|
|
61
|
+
Use `previewEquipmentTemplate` before `applyEquipmentTemplate` for fleet-wide setup.
|
|
62
|
+
|
|
63
|
+
With the matching API update deployed, `createMaintenance` also accepts
|
|
64
|
+
`forecastConsent: true` and a `forecast` object containing `baselineDate`,
|
|
65
|
+
`baselineMileage`, `intervalMiles`, `annualMileage` and `reminderDays` (0 means on
|
|
66
|
+
the estimated date). Supply the actual last check, not an assumed MOT check date.
|
|
67
|
+
Duplicate setup does not create another reminder; refresh the maintenance list.
|
|
68
|
+
`completeMaintenance` accepts `completedDate` and `completedMileage` for
|
|
69
|
+
an existing forecast. Supply the identical body for preview and confirmation;
|
|
70
|
+
the next cycle retains the saved mileage interval and reminder timing. Ordinary
|
|
71
|
+
maintenance updates cannot edit calculated forecast dates. There are no automatic
|
|
72
|
+
fuel-card feeds in the CLI. Wex credentials and Xero authorisation are entered in
|
|
73
|
+
the website, never through CLI command arguments.
|
|
74
|
+
|
|
8
75
|
## Safety model
|
|
9
76
|
|
|
10
77
|
- `api operations` is generated as a fixed allowlist of the published OpenAPI operation IDs.
|
package/api-command.mjs
CHANGED
|
@@ -97,6 +97,9 @@ function operationPath(operation, assignments) {
|
|
|
97
97
|
if (!values.has(name) || values.get(name) === '') {
|
|
98
98
|
throw new Error(`${operation.id} requires --path ${name}=<value>.`);
|
|
99
99
|
}
|
|
100
|
+
if (values.get(name) === '.' || values.get(name) === '..') {
|
|
101
|
+
throw new Error(`--path ${name} must not be a dot segment.`);
|
|
102
|
+
}
|
|
100
103
|
path = path.replace(`{${name}}`, encodeURIComponent(values.get(name)));
|
|
101
104
|
}
|
|
102
105
|
return path;
|
|
@@ -280,9 +283,9 @@ export async function runApiCommand(args) {
|
|
|
280
283
|
throw new Error('Use `fleetkeep api operations` or `fleetkeep api call`.');
|
|
281
284
|
const operationId = args[1];
|
|
282
285
|
if (!operationId || operationId.startsWith('--')) throw new Error('An operation ID is required.');
|
|
283
|
-
|
|
284
|
-
if (!operation) {
|
|
286
|
+
if (!Object.hasOwn(operations, operationId)) {
|
|
285
287
|
throw new Error(`Unknown or unpublished Fleetkeep operation: ${operationId}.`);
|
|
286
288
|
}
|
|
289
|
+
const operation = operations[operationId];
|
|
287
290
|
await callOperation(operation, args.slice(2));
|
|
288
291
|
}
|
package/fleetkeep.mjs
CHANGED
|
@@ -21,9 +21,18 @@ Usage:
|
|
|
21
21
|
fleetkeep vehicles delete <vehicle-id> [--confirmation <server-token>] [--json]
|
|
22
22
|
fleetkeep vehicles bulk-add <registration>... [--json]
|
|
23
23
|
fleetkeep drivers list [--json]
|
|
24
|
+
fleetkeep drivers activity [--search <job-driver-registration>] [--json]
|
|
25
|
+
fleetkeep costs list [--from <YYYY-MM-DD>] [--to <YYYY-MM-DD>] [--json]
|
|
26
|
+
fleetkeep costs record --data-file <expense.json>
|
|
27
|
+
fleetkeep tracking list [--json]
|
|
28
|
+
fleetkeep tracking route <vehicle-id> [--period <period>] [--json]
|
|
24
29
|
fleetkeep drivers invite <email> --name <name> [--json]
|
|
25
30
|
fleetkeep drivers resend <driver-id> [--json]
|
|
26
31
|
fleetkeep drivers revoke <driver-id> [--json]
|
|
32
|
+
fleetkeep jobs list [--search <reference>] [--json]
|
|
33
|
+
fleetkeep jobs save <reference> --title <title> [--json]
|
|
34
|
+
fleetkeep jobs close <job-id> [--json]
|
|
35
|
+
fleetkeep jobs reopen <job-id> [--json]
|
|
27
36
|
fleetkeep import preview <file.csv|file.xlsx> [--json]
|
|
28
37
|
fleetkeep import apply <file.csv|file.xlsx> --confirm <digest> [--idempotency-key <key>] [--json]
|
|
29
38
|
fleetkeep api operations [--json]
|
|
@@ -47,6 +56,8 @@ Vehicle options:
|
|
|
47
56
|
--last-service-date <YYYY-MM-DD>
|
|
48
57
|
--last-service-mileage <integer>
|
|
49
58
|
--service-interval-months <integer>
|
|
59
|
+
--service-interval-days <integer>
|
|
60
|
+
--service-interval-weeks <integer>
|
|
50
61
|
--operational-status <active|standby|in_maintenance|off_road|sold_or_removed>
|
|
51
62
|
--assigned-driver-name <text>
|
|
52
63
|
--assigned-driver-contact <text>
|
|
@@ -54,7 +65,10 @@ Vehicle options:
|
|
|
54
65
|
|
|
55
66
|
Clearable update fields:
|
|
56
67
|
label, insuranceDueDate, lastServiceDate, lastServiceMileage, serviceIntervalMonths,
|
|
57
|
-
assignedDriverName, assignedDriverContact, statusNotes
|
|
68
|
+
serviceIntervalDays, assignedDriverName, assignedDriverContact, statusNotes
|
|
69
|
+
|
|
70
|
+
Choose one service interval unit. Weeks are stored as days. Setting an interval
|
|
71
|
+
clears the alternative unit so an old interval cannot override the new one.
|
|
58
72
|
|
|
59
73
|
Environment:
|
|
60
74
|
FLEETKEEP_ACCESS_TOKEN Optional OAuth token override for automation
|
|
@@ -103,6 +117,11 @@ const VEHICLE_OPTIONS = new Map([
|
|
|
103
117
|
'--service-interval-months',
|
|
104
118
|
{ field: 'serviceIntervalMonths', type: 'integer', min: 1, max: 120 }
|
|
105
119
|
],
|
|
120
|
+
['--service-interval-days', { field: 'serviceIntervalDays', type: 'integer', min: 1, max: 3650 }],
|
|
121
|
+
[
|
|
122
|
+
'--service-interval-weeks',
|
|
123
|
+
{ field: 'serviceIntervalDays', type: 'integer', min: 1, max: 521, multiplier: 7 }
|
|
124
|
+
],
|
|
106
125
|
['--operational-status', { field: 'operationalStatus', type: 'status' }],
|
|
107
126
|
['--assigned-driver-name', { field: 'assignedDriverName', type: 'string' }],
|
|
108
127
|
['--assigned-driver-contact', { field: 'assignedDriverContact', type: 'string' }],
|
|
@@ -151,11 +170,22 @@ function parseVehicleValue(name, value, config) {
|
|
|
151
170
|
|
|
152
171
|
function vehicleBody(args, { allowClear = false } = {}) {
|
|
153
172
|
const body = {};
|
|
173
|
+
const intervalOptions = [
|
|
174
|
+
'--service-interval-days',
|
|
175
|
+
'--service-interval-weeks',
|
|
176
|
+
'--service-interval-months'
|
|
177
|
+
];
|
|
178
|
+
if (intervalOptions.filter((name) => args.includes(name)).length > 1) {
|
|
179
|
+
throw new Error('Choose one service interval unit: days, weeks or months.');
|
|
180
|
+
}
|
|
154
181
|
const recognised = new Set(['--json']);
|
|
155
182
|
for (const [name, config] of VEHICLE_OPTIONS) {
|
|
156
183
|
recognised.add(name);
|
|
157
184
|
const value = requiredValue(args, name);
|
|
158
|
-
if (value !== null)
|
|
185
|
+
if (value !== null) {
|
|
186
|
+
const parsed = parseVehicleValue(name, value, config);
|
|
187
|
+
body[config.field] = config.multiplier ? parsed * config.multiplier : parsed;
|
|
188
|
+
}
|
|
159
189
|
}
|
|
160
190
|
|
|
161
191
|
if (allowClear) {
|
|
@@ -180,6 +210,8 @@ function vehicleBody(args, { allowClear = false } = {}) {
|
|
|
180
210
|
for (const arg of args) {
|
|
181
211
|
if (arg.startsWith('--') && !recognised.has(arg)) throw new Error(`Unknown option: ${arg}.`);
|
|
182
212
|
}
|
|
213
|
+
if (body.serviceIntervalDays != null) body.serviceIntervalMonths = null;
|
|
214
|
+
if (body.serviceIntervalMonths != null) body.serviceIntervalDays = null;
|
|
183
215
|
return body;
|
|
184
216
|
}
|
|
185
217
|
|
|
@@ -293,6 +325,46 @@ async function main() {
|
|
|
293
325
|
return;
|
|
294
326
|
}
|
|
295
327
|
|
|
328
|
+
if (args[0] === 'costs' && args[1] === 'list') {
|
|
329
|
+
const query = [];
|
|
330
|
+
for (const name of ['from', 'to']) {
|
|
331
|
+
const value = option(args, `--${name}`);
|
|
332
|
+
if (value) query.push('--query', `${name}=${value}`);
|
|
333
|
+
}
|
|
334
|
+
await runApiCommand(['call', 'getRunningCosts', ...query]);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (args[0] === 'costs' && args[1] === 'record') {
|
|
338
|
+
const file = option(args, '--data-file');
|
|
339
|
+
if (!file) throw new Error('--data-file is required. Reuse the expense UUID on retries.');
|
|
340
|
+
await runApiCommand(['call', 'recordFleetExpense', '--data-file', file]);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (args[0] === 'tracking' && args[1] === 'list') {
|
|
344
|
+
await runApiCommand(['call', 'listTrackingSnapshots']);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (args[0] === 'tracking' && args[1] === 'route') {
|
|
348
|
+
if (!args[2] || args[2].startsWith('--')) throw new Error('A vehicle ID is required.');
|
|
349
|
+
const period = option(args, '--period');
|
|
350
|
+
await runApiCommand([
|
|
351
|
+
'call',
|
|
352
|
+
'getWexRouteHistory',
|
|
353
|
+
'--path',
|
|
354
|
+
`vehicleId=${args[2]}`,
|
|
355
|
+
...(period ? ['--query', `period=${period}`] : [])
|
|
356
|
+
]);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (args[0] === 'drivers' && args[1] === 'activity') {
|
|
360
|
+
const payload = await request(
|
|
361
|
+
`/api/v1/drivers/activity?q=${encodeURIComponent(option(args, '--search') || '')}`,
|
|
362
|
+
{}
|
|
363
|
+
);
|
|
364
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
296
368
|
if (args[0] === 'auth' && args[1] === 'login') {
|
|
297
369
|
await login({ noBrowser: args.includes('--no-browser') });
|
|
298
370
|
return;
|
|
@@ -409,6 +481,30 @@ async function main() {
|
|
|
409
481
|
return;
|
|
410
482
|
}
|
|
411
483
|
|
|
484
|
+
if (args[0] === 'jobs' && ['list', 'save', 'close', 'reopen'].includes(args[1])) {
|
|
485
|
+
const action = args[1];
|
|
486
|
+
let payload;
|
|
487
|
+
if (action === 'list') {
|
|
488
|
+
payload = await request(
|
|
489
|
+
`/api/v1/jobs?q=${encodeURIComponent(option(args, '--search') || '')}`,
|
|
490
|
+
{}
|
|
491
|
+
);
|
|
492
|
+
} else {
|
|
493
|
+
if (!args[2] || args[2].startsWith('--'))
|
|
494
|
+
throw new Error('A job reference or ID is required.');
|
|
495
|
+
if (action === 'save' && !option(args, '--title')) throw new Error('--title is required.');
|
|
496
|
+
payload = await request('/api/v1/jobs', {
|
|
497
|
+
method: 'POST',
|
|
498
|
+
body:
|
|
499
|
+
action === 'save'
|
|
500
|
+
? { action: 'save', reference: args[2], title: option(args, '--title') }
|
|
501
|
+
: { action: 'status', id: args[2], status: action === 'close' ? 'closed' : 'active' }
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
412
508
|
if (args[0] === 'drivers' && args[1] === 'list') {
|
|
413
509
|
const payload = await request('/api/v1/drivers', {});
|
|
414
510
|
if (jsonOutput) process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
package/operations.mjs
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
/** @type {Array<[string, string, string, string, Record<string, unknown>?]>} */
|
|
2
2
|
const definitions = [
|
|
3
|
+
['importProviderJobs', 'POST', '/api/v1/jobs/import', 'operations:write'],
|
|
4
|
+
['listFleetJobs', 'GET', '/api/v1/jobs', 'operations:read', { query: ['q'] }],
|
|
5
|
+
['saveFleetJob', 'POST', '/api/v1/jobs', 'operations:write'],
|
|
6
|
+
['getRunningCosts', 'GET', '/api/v1/costs', 'operations:read', { query: ['from', 'to'] }],
|
|
7
|
+
['recordFleetExpense', 'POST', '/api/v1/costs', 'operations:write'],
|
|
8
|
+
[
|
|
9
|
+
'listDriverJobs',
|
|
10
|
+
'GET',
|
|
11
|
+
'/api/v1/driver-workspace/{id}/jobs',
|
|
12
|
+
'operations:read',
|
|
13
|
+
{ query: ['q'] }
|
|
14
|
+
],
|
|
3
15
|
['listVehicles', 'GET', '/api/v1/vehicles', 'vehicles:read'],
|
|
4
16
|
['addVehicle', 'POST', '/api/v1/vehicles', 'vehicles:write'],
|
|
5
17
|
['getVehicle', 'GET', '/api/v1/vehicles/{id}', 'vehicles:read'],
|
|
@@ -22,6 +34,7 @@ const definitions = [
|
|
|
22
34
|
['previewVehicleImport', 'POST', '/api/v1/imports/vehicles/preview', 'vehicles:read'],
|
|
23
35
|
['applyVehicleImport', 'POST', '/api/v1/imports/vehicles/apply', 'vehicles:write'],
|
|
24
36
|
['getWalkaroundDefinition', 'GET', '/api/v1/operations/walkaround-definition', 'operations:read'],
|
|
37
|
+
['getWalkaroundSummary', 'GET', '/api/v1/operations/walkaround-summary', 'operations:read'],
|
|
25
38
|
[
|
|
26
39
|
'listWalkarounds',
|
|
27
40
|
'GET',
|