@velaro/cli 1.4.31 → 1.4.32
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/bin/velaro.js +2 -0
- package/lib/commands/booking.js +279 -0
- package/package.json +1 -1
package/bin/velaro.js
CHANGED
|
@@ -36,6 +36,7 @@ import { acuityCommand } from '../lib/commands/acuity.js';
|
|
|
36
36
|
import { avatarCommand } from '../lib/commands/avatar.js';
|
|
37
37
|
import { integrationCommand } from '../lib/commands/integration.js';
|
|
38
38
|
import { appointmentsCommand } from '../lib/commands/appointments.js';
|
|
39
|
+
import { bookingCommand } from '../lib/commands/booking.js';
|
|
39
40
|
import { reportCommand } from '../lib/commands/report.js';
|
|
40
41
|
import { conversationEfficiencyCommand } from '../lib/commands/conversation-efficiency.js';
|
|
41
42
|
import { dbCommand } from '../lib/commands/db.js';
|
|
@@ -128,6 +129,7 @@ await yargs(hideBin(process.argv))
|
|
|
128
129
|
.command(avatarCommand)
|
|
129
130
|
.command(integrationCommand)
|
|
130
131
|
.command(appointmentsCommand)
|
|
132
|
+
.command(bookingCommand)
|
|
131
133
|
.command(reportCommand)
|
|
132
134
|
.command(conversationEfficiencyCommand)
|
|
133
135
|
.command(dbCommand)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { messagingGet, messagingPost, messagingPut, messagingDel } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
// ── velaro booking ────────────────────────────────────────────────────────────
|
|
5
|
+
// CLI surface for the generic booking engine (Phase 1b) -- see velaro-admin
|
|
6
|
+
// memory project_2026_09_06_generic_booking_engine_schema_plan.md. Backend:
|
|
7
|
+
// server/Velaro.Messaging/Controllers/{BookableResourcesController,
|
|
8
|
+
// BookableServicesController,ResourceAvailabilityRulesController}.cs, routed
|
|
9
|
+
// at /BookableResources, /BookableServices, /ResourceAvailabilityRules.
|
|
10
|
+
// Mirrors the booking_* MCP tools. SiteId always derives from the caller's
|
|
11
|
+
// JWT server-side -- never accepted as a CLI argument.
|
|
12
|
+
|
|
13
|
+
const RESOURCE_TYPES = ['Staff', 'Room', 'Equipment', 'Virtual'];
|
|
14
|
+
|
|
15
|
+
function fmtDate(d) { return d ? new Date(d).toISOString().replace('T', ' ').slice(0, 16) : '-'; }
|
|
16
|
+
function fmtTime(t) { return t == null ? '-' : String(t); }
|
|
17
|
+
|
|
18
|
+
export const bookingCommand = {
|
|
19
|
+
command: 'booking <subcommand>',
|
|
20
|
+
describe: 'Generic booking engine -- bookable resources, services, and availability rules',
|
|
21
|
+
builder: yargs => yargs
|
|
22
|
+
// ── resource ────────────────────────────────────────────────────────────
|
|
23
|
+
.command({
|
|
24
|
+
command: 'resource <action>',
|
|
25
|
+
describe: 'Manage bookable resources (staff / room / equipment / virtual)',
|
|
26
|
+
builder: y => y
|
|
27
|
+
.command({
|
|
28
|
+
command: 'list',
|
|
29
|
+
describe: 'List bookable resources for this site',
|
|
30
|
+
handler: runCommand(async () => {
|
|
31
|
+
const resources = await messagingGet('/BookableResources');
|
|
32
|
+
if (!Array.isArray(resources) || !resources.length) {
|
|
33
|
+
console.log('\n No bookable resources found.\n');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.log('\nBookable Resources\n');
|
|
37
|
+
for (const r of resources) {
|
|
38
|
+
console.log(` id:${r.id} ${r.name} type:${r.resourceType} active:${r.isActive}`);
|
|
39
|
+
}
|
|
40
|
+
console.log('');
|
|
41
|
+
}),
|
|
42
|
+
})
|
|
43
|
+
.command({
|
|
44
|
+
command: 'get <id>',
|
|
45
|
+
describe: 'Get a single bookable resource by id',
|
|
46
|
+
builder: y2 => y2.positional('id', { type: 'number', describe: 'Resource id' }),
|
|
47
|
+
handler: runCommand(async argv => {
|
|
48
|
+
const resource = await messagingGet(`/BookableResources/${argv.id}`);
|
|
49
|
+
console.log(JSON.stringify(resource, null, 2));
|
|
50
|
+
}),
|
|
51
|
+
})
|
|
52
|
+
.command({
|
|
53
|
+
command: 'create',
|
|
54
|
+
describe: 'Create a bookable resource',
|
|
55
|
+
builder: y2 => y2
|
|
56
|
+
.option('name', { type: 'string', demandOption: true, describe: 'Resource name -- required' })
|
|
57
|
+
.option('type', { type: 'string', choices: RESOURCE_TYPES, describe: 'Resource type (Staff | Room | Equipment | Virtual, default Staff)' })
|
|
58
|
+
.option('active', { type: 'boolean', describe: 'Whether the resource is active (default true)' }),
|
|
59
|
+
handler: runCommand(async argv => {
|
|
60
|
+
const resource = await messagingPost('/BookableResources', {
|
|
61
|
+
name: argv.name,
|
|
62
|
+
resourceType: argv.type,
|
|
63
|
+
isActive: argv.active,
|
|
64
|
+
});
|
|
65
|
+
console.log(`\n✓ Created resource id:${resource.id} "${resource.name}"\n`);
|
|
66
|
+
}),
|
|
67
|
+
})
|
|
68
|
+
.command({
|
|
69
|
+
command: 'update <id>',
|
|
70
|
+
describe: 'Update a bookable resource -- only supplied fields change',
|
|
71
|
+
builder: y2 => y2
|
|
72
|
+
.positional('id', { type: 'number', describe: 'Resource id' })
|
|
73
|
+
.option('name', { type: 'string', describe: 'New name' })
|
|
74
|
+
.option('type', { type: 'string', choices: RESOURCE_TYPES, describe: 'New resource type' })
|
|
75
|
+
.option('active', { type: 'boolean', describe: 'New active state' }),
|
|
76
|
+
handler: runCommand(async argv => {
|
|
77
|
+
const resource = await messagingPut(`/BookableResources/${argv.id}`, {
|
|
78
|
+
name: argv.name,
|
|
79
|
+
resourceType: argv.type,
|
|
80
|
+
isActive: argv.active,
|
|
81
|
+
});
|
|
82
|
+
console.log(`\n✓ Updated resource id:${resource.id} "${resource.name}"\n`);
|
|
83
|
+
}),
|
|
84
|
+
})
|
|
85
|
+
.command({
|
|
86
|
+
command: 'delete <id>',
|
|
87
|
+
describe: 'Delete a bookable resource',
|
|
88
|
+
builder: y2 => y2.positional('id', { type: 'number', describe: 'Resource id' }),
|
|
89
|
+
handler: runCommand(async argv => {
|
|
90
|
+
await messagingDel(`/BookableResources/${argv.id}`);
|
|
91
|
+
console.log(`\n✓ Deleted resource id:${argv.id}\n`);
|
|
92
|
+
}),
|
|
93
|
+
})
|
|
94
|
+
.demandCommand(1, 'Specify a resource action: list | get | create | update | delete'),
|
|
95
|
+
})
|
|
96
|
+
// ── service ─────────────────────────────────────────────────────────────
|
|
97
|
+
.command({
|
|
98
|
+
command: 'service <action>',
|
|
99
|
+
describe: 'Manage bookable services (the "thing" a customer books)',
|
|
100
|
+
builder: y => y
|
|
101
|
+
.command({
|
|
102
|
+
command: 'list',
|
|
103
|
+
describe: 'List bookable services for this site',
|
|
104
|
+
handler: runCommand(async () => {
|
|
105
|
+
const services = await messagingGet('/BookableServices');
|
|
106
|
+
if (!Array.isArray(services) || !services.length) {
|
|
107
|
+
console.log('\n No bookable services found.\n');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
console.log('\nBookable Services\n');
|
|
111
|
+
for (const s of services) {
|
|
112
|
+
console.log(` id:${s.id} ${s.name} ${s.durationMinutes}min capacity:${s.defaultCapacity} requiresResource:${s.requiresResource} category:${s.category ?? '-'} active:${s.isActive}`);
|
|
113
|
+
}
|
|
114
|
+
console.log('');
|
|
115
|
+
}),
|
|
116
|
+
})
|
|
117
|
+
.command({
|
|
118
|
+
command: 'get <id>',
|
|
119
|
+
describe: 'Get a single bookable service by id',
|
|
120
|
+
builder: y2 => y2.positional('id', { type: 'number', describe: 'Service id' }),
|
|
121
|
+
handler: runCommand(async argv => {
|
|
122
|
+
const service = await messagingGet(`/BookableServices/${argv.id}`);
|
|
123
|
+
console.log(JSON.stringify(service, null, 2));
|
|
124
|
+
}),
|
|
125
|
+
})
|
|
126
|
+
.command({
|
|
127
|
+
command: 'create',
|
|
128
|
+
describe: 'Create a bookable service',
|
|
129
|
+
builder: y2 => y2
|
|
130
|
+
.option('name', { type: 'string', demandOption: true, describe: 'Service name -- required' })
|
|
131
|
+
.option('duration', { type: 'number', describe: 'Duration in minutes (default 30)' })
|
|
132
|
+
.option('capacity', { type: 'number', describe: 'Default seats per slot (default 1)' })
|
|
133
|
+
.option('requires-resource', { type: 'boolean', describe: 'Whether a slot must be tied to a specific resource (default true)' })
|
|
134
|
+
.option('category', { type: 'string', describe: 'Free-text grouping, e.g. "Hair" or "Fitness Classes"' })
|
|
135
|
+
.option('active', { type: 'boolean', describe: 'Whether the service is active (default true)' }),
|
|
136
|
+
handler: runCommand(async argv => {
|
|
137
|
+
const service = await messagingPost('/BookableServices', {
|
|
138
|
+
name: argv.name,
|
|
139
|
+
durationMinutes: argv.duration,
|
|
140
|
+
defaultCapacity: argv.capacity,
|
|
141
|
+
requiresResource: argv.requiresResource,
|
|
142
|
+
category: argv.category,
|
|
143
|
+
isActive: argv.active,
|
|
144
|
+
});
|
|
145
|
+
console.log(`\n✓ Created service id:${service.id} "${service.name}"\n`);
|
|
146
|
+
}),
|
|
147
|
+
})
|
|
148
|
+
.command({
|
|
149
|
+
command: 'update <id>',
|
|
150
|
+
describe: 'Update a bookable service -- only supplied fields change',
|
|
151
|
+
builder: y2 => y2
|
|
152
|
+
.positional('id', { type: 'number', describe: 'Service id' })
|
|
153
|
+
.option('name', { type: 'string', describe: 'New name' })
|
|
154
|
+
.option('duration', { type: 'number', describe: 'New duration in minutes' })
|
|
155
|
+
.option('capacity', { type: 'number', describe: 'New default seats per slot' })
|
|
156
|
+
.option('requires-resource', { type: 'boolean', describe: 'New requires-resource flag' })
|
|
157
|
+
.option('category', { type: 'string', describe: 'New category (pass "" to clear)' })
|
|
158
|
+
.option('active', { type: 'boolean', describe: 'New active state' }),
|
|
159
|
+
handler: runCommand(async argv => {
|
|
160
|
+
const service = await messagingPut(`/BookableServices/${argv.id}`, {
|
|
161
|
+
name: argv.name,
|
|
162
|
+
durationMinutes: argv.duration,
|
|
163
|
+
defaultCapacity: argv.capacity,
|
|
164
|
+
requiresResource: argv.requiresResource,
|
|
165
|
+
category: argv.category,
|
|
166
|
+
isActive: argv.active,
|
|
167
|
+
});
|
|
168
|
+
console.log(`\n✓ Updated service id:${service.id} "${service.name}"\n`);
|
|
169
|
+
}),
|
|
170
|
+
})
|
|
171
|
+
.command({
|
|
172
|
+
command: 'delete <id>',
|
|
173
|
+
describe: 'Delete a bookable service',
|
|
174
|
+
builder: y2 => y2.positional('id', { type: 'number', describe: 'Service id' }),
|
|
175
|
+
handler: runCommand(async argv => {
|
|
176
|
+
await messagingDel(`/BookableServices/${argv.id}`);
|
|
177
|
+
console.log(`\n✓ Deleted service id:${argv.id}\n`);
|
|
178
|
+
}),
|
|
179
|
+
})
|
|
180
|
+
.demandCommand(1, 'Specify a service action: list | get | create | update | delete'),
|
|
181
|
+
})
|
|
182
|
+
// ── availability ────────────────────────────────────────────────────────
|
|
183
|
+
.command({
|
|
184
|
+
command: 'availability <action>',
|
|
185
|
+
describe: 'Manage weekly-recurring resource availability rules',
|
|
186
|
+
builder: y => y
|
|
187
|
+
.command({
|
|
188
|
+
command: 'list',
|
|
189
|
+
describe: 'List availability rules for this site, optionally filtered to one resource',
|
|
190
|
+
builder: y2 => y2.option('resource-id', { type: 'number', describe: 'Only show rules for this resource id' }),
|
|
191
|
+
handler: runCommand(async argv => {
|
|
192
|
+
const qs = argv.resourceId != null ? `?resourceId=${encodeURIComponent(argv.resourceId)}` : '';
|
|
193
|
+
const rules = await messagingGet(`/ResourceAvailabilityRules${qs}`);
|
|
194
|
+
if (!Array.isArray(rules) || !rules.length) {
|
|
195
|
+
console.log('\n No availability rules found.\n');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
console.log('\nAvailability Rules\n');
|
|
199
|
+
for (const r of rules) {
|
|
200
|
+
console.log(` id:${r.id} resource:${r.resourceId ?? '(site default)'} ${r.dayOfWeek} ${fmtTime(r.startLocal)}-${fmtTime(r.endLocal)} ${r.ianaTimeZone} from:${fmtDate(r.effectiveFrom)} to:${fmtDate(r.effectiveTo)} active:${r.isActive}`);
|
|
201
|
+
}
|
|
202
|
+
console.log('');
|
|
203
|
+
}),
|
|
204
|
+
})
|
|
205
|
+
.command({
|
|
206
|
+
command: 'get <id>',
|
|
207
|
+
describe: 'Get a single availability rule by id',
|
|
208
|
+
builder: y2 => y2.positional('id', { type: 'number', describe: 'Rule id' }),
|
|
209
|
+
handler: runCommand(async argv => {
|
|
210
|
+
const rule = await messagingGet(`/ResourceAvailabilityRules/${argv.id}`);
|
|
211
|
+
console.log(JSON.stringify(rule, null, 2));
|
|
212
|
+
}),
|
|
213
|
+
})
|
|
214
|
+
.command({
|
|
215
|
+
command: 'create',
|
|
216
|
+
describe: 'Create a weekly-recurring availability rule',
|
|
217
|
+
builder: y2 => y2
|
|
218
|
+
.option('resource-id', { type: 'number', describe: 'Resource id this rule applies to. Omit for a site-wide default.' })
|
|
219
|
+
.option('day', { type: 'string', demandOption: true, describe: 'Day of week (Sunday..Saturday) -- required' })
|
|
220
|
+
.option('start', { type: 'string', demandOption: true, describe: 'Local start time, HH:MM:SS -- required' })
|
|
221
|
+
.option('end', { type: 'string', demandOption: true, describe: 'Local end time, HH:MM:SS -- required, must be after start' })
|
|
222
|
+
.option('timezone', { type: 'string', describe: 'IANA timezone id (default America/Los_Angeles)' })
|
|
223
|
+
.option('effective-from', { type: 'string', describe: 'ISO 8601 timestamp the rule starts applying (default now)' })
|
|
224
|
+
.option('effective-to', { type: 'string', describe: 'ISO 8601 timestamp the rule stops applying (default open-ended)' })
|
|
225
|
+
.option('active', { type: 'boolean', describe: 'Whether the rule is active (default true)' }),
|
|
226
|
+
handler: runCommand(async argv => {
|
|
227
|
+
const rule = await messagingPost('/ResourceAvailabilityRules', {
|
|
228
|
+
resourceId: argv.resourceId,
|
|
229
|
+
dayOfWeek: argv.day,
|
|
230
|
+
startLocal: argv.start,
|
|
231
|
+
endLocal: argv.end,
|
|
232
|
+
ianaTimeZone: argv.timezone,
|
|
233
|
+
effectiveFrom: argv.effectiveFrom,
|
|
234
|
+
effectiveTo: argv.effectiveTo,
|
|
235
|
+
isActive: argv.active,
|
|
236
|
+
});
|
|
237
|
+
console.log(`\n✓ Created availability rule id:${rule.id}\n`);
|
|
238
|
+
}),
|
|
239
|
+
})
|
|
240
|
+
.command({
|
|
241
|
+
command: 'update <id>',
|
|
242
|
+
describe: 'Update an availability rule -- only supplied fields change',
|
|
243
|
+
builder: y2 => y2
|
|
244
|
+
.positional('id', { type: 'number', describe: 'Rule id' })
|
|
245
|
+
.option('resource-id', { type: 'number', describe: 'New resource id' })
|
|
246
|
+
.option('day', { type: 'string', describe: 'New day of week' })
|
|
247
|
+
.option('start', { type: 'string', describe: 'New local start time, HH:MM:SS' })
|
|
248
|
+
.option('end', { type: 'string', describe: 'New local end time, HH:MM:SS' })
|
|
249
|
+
.option('timezone', { type: 'string', describe: 'New IANA timezone id' })
|
|
250
|
+
.option('effective-from', { type: 'string', describe: 'New effective-from ISO 8601 timestamp' })
|
|
251
|
+
.option('effective-to', { type: 'string', describe: 'New effective-to ISO 8601 timestamp' })
|
|
252
|
+
.option('active', { type: 'boolean', describe: 'New active state' }),
|
|
253
|
+
handler: runCommand(async argv => {
|
|
254
|
+
const rule = await messagingPut(`/ResourceAvailabilityRules/${argv.id}`, {
|
|
255
|
+
resourceId: argv.resourceId,
|
|
256
|
+
dayOfWeek: argv.day,
|
|
257
|
+
startLocal: argv.start,
|
|
258
|
+
endLocal: argv.end,
|
|
259
|
+
ianaTimeZone: argv.timezone,
|
|
260
|
+
effectiveFrom: argv.effectiveFrom,
|
|
261
|
+
effectiveTo: argv.effectiveTo,
|
|
262
|
+
isActive: argv.active,
|
|
263
|
+
});
|
|
264
|
+
console.log(`\n✓ Updated availability rule id:${rule.id}\n`);
|
|
265
|
+
}),
|
|
266
|
+
})
|
|
267
|
+
.command({
|
|
268
|
+
command: 'delete <id>',
|
|
269
|
+
describe: 'Delete an availability rule',
|
|
270
|
+
builder: y2 => y2.positional('id', { type: 'number', describe: 'Rule id' }),
|
|
271
|
+
handler: runCommand(async argv => {
|
|
272
|
+
await messagingDel(`/ResourceAvailabilityRules/${argv.id}`);
|
|
273
|
+
console.log(`\n✓ Deleted availability rule id:${argv.id}\n`);
|
|
274
|
+
}),
|
|
275
|
+
})
|
|
276
|
+
.demandCommand(1, 'Specify an availability action: list | get | create | update | delete'),
|
|
277
|
+
})
|
|
278
|
+
.demandCommand(1, 'Specify a subcommand: resource | service | availability'),
|
|
279
|
+
};
|
package/package.json
CHANGED