@velaro/cli 1.2.0 → 1.4.9
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 +161 -138
- package/bin/velaro.js +177 -62
- package/lib/api.js +91 -52
- package/lib/api.test.js +46 -0
- package/lib/banner.js +76 -0
- package/lib/commands/activity.js +133 -0
- package/lib/commands/acuity.js +66 -0
- package/lib/commands/agent.js +204 -50
- package/lib/commands/ai-config.js +193 -0
- package/lib/commands/ai-models.js +159 -0
- package/lib/commands/appointments.js +198 -0
- package/lib/commands/article.js +668 -388
- package/lib/commands/automation-draft.js +134 -0
- package/lib/commands/avatar.js +75 -0
- package/lib/commands/bigcommerce.js +50 -0
- package/lib/commands/billing-contacts.js +62 -0
- package/lib/commands/billing-email-preference.js +64 -0
- package/lib/commands/billing-subscription.js +265 -0
- package/lib/commands/billing.js +138 -0
- package/lib/commands/bot.js +141 -137
- package/lib/commands/bundle.js +168 -0
- package/lib/commands/calendly.js +62 -0
- package/lib/commands/callback.js +125 -0
- package/lib/commands/callrail.js +88 -0
- package/lib/commands/campaigns.js +44 -0
- package/lib/commands/case.js +102 -0
- package/lib/commands/check.js +163 -163
- package/lib/commands/compliance.js +229 -0
- package/lib/commands/conversation-efficiency.js +178 -0
- package/lib/commands/copilotstudio.js +114 -0
- package/lib/commands/coupon-grant.js +192 -0
- package/lib/commands/db.js +101 -0
- package/lib/commands/deployment.js +107 -107
- package/lib/commands/diagnostics.js +298 -0
- package/lib/commands/email-campaign.js +47 -0
- package/lib/commands/email-inbox.js +88 -0
- package/lib/commands/entitlement.js +176 -0
- package/lib/commands/env.js +45 -45
- package/lib/commands/feature-discovery.js +40 -0
- package/lib/commands/focus.js +278 -0
- package/lib/commands/index.js +38 -5
- package/lib/commands/ingest.js +31 -31
- package/lib/commands/inline-widget-config.js +126 -0
- package/lib/commands/integration.js +93 -0
- package/lib/commands/kb.js +450 -309
- package/lib/commands/login.js +86 -86
- package/lib/commands/logs.js +680 -0
- package/lib/commands/magento.js +210 -0
- package/lib/commands/mcp-key.js +188 -159
- package/lib/commands/migrate.js +134 -0
- package/lib/commands/migration-status.js +66 -0
- package/lib/commands/monday.js +137 -0
- package/lib/commands/netsuite.js +87 -0
- package/lib/commands/notifications.js +63 -0
- package/lib/commands/notion.js +70 -0
- package/lib/commands/ops.js +267 -173
- package/lib/commands/payment-recovery.js +170 -0
- package/lib/commands/pickup.js +172 -0
- package/lib/commands/pricing.js +132 -0
- package/lib/commands/product.js +55 -0
- package/lib/commands/recruiting.js +374 -0
- package/lib/commands/report.js +462 -0
- package/lib/commands/routing.js +304 -0
- package/lib/commands/rule.js +85 -85
- package/lib/commands/sharepoint.js +167 -0
- package/lib/commands/site-provision.js +68 -0
- package/lib/commands/site.js +62 -62
- package/lib/commands/sitesync.js +158 -0
- package/lib/commands/slack.js +64 -0
- package/lib/commands/squarespace.js +108 -0
- package/lib/commands/status.js +24 -24
- package/lib/commands/subscription.js +43 -0
- package/lib/commands/support.js +128 -0
- package/lib/commands/survey.js +216 -0
- package/lib/commands/team.js +144 -144
- package/lib/commands/teams-phone.js +131 -0
- package/lib/commands/teams.js +106 -0
- package/lib/commands/telephony.js +99 -0
- package/lib/commands/update.js +47 -47
- package/lib/commands/webflow.js +128 -0
- package/lib/commands/whoami.js +25 -22
- package/lib/commands/widget-container.js +152 -0
- package/lib/commands/woocommerce.js +240 -0
- package/lib/commands/workflow.js +233 -98
- package/lib/config.js +85 -83
- package/lib/kb-screenshot.js +320 -0
- package/lib/migrations/amscro.json +72 -0
- package/lib/migrations/azenta.json +68 -0
- package/lib/migrations/bluefire.json +49 -0
- package/lib/migrations/donaldson.json +75 -0
- package/lib/oauth.js +149 -135
- package/lib/run.js +21 -16
- package/lib/sharepoint-auth.js +138 -0
- package/lib/subscription.js +41 -39
- package/lib/track.js +35 -35
- package/lib/update-check.js +64 -64
- package/package.json +34 -19
- package/scripts/postinstall.js +12 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { messagingGet, messagingPost, messagingPut } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
// ── velaro pickup ──────────────────────────────────────────────────────────────
|
|
5
|
+
// CLI surface for "Pickup & Reminder Texting" -- notify a contact their order/item
|
|
6
|
+
// is ready, optionally with recurring reminders, mark it picked up, view history,
|
|
7
|
+
// and manage the per-site config. siteId is applied server-side from the
|
|
8
|
+
// vmsg-siteId header (see cli/lib/api.js messagingRequest) -- never a CLI arg.
|
|
9
|
+
//
|
|
10
|
+
// Backend contract: server/Velaro.Messaging/Controllers/PickupNotificationsController.cs
|
|
11
|
+
// POST PickupNotifications/Contacts/{contactId}/notify { message, startReminderChain, cadenceHours?, maxReminders? } -> { conversationBaseId, workflowJobId }
|
|
12
|
+
// POST PickupNotifications/Conversations/{conversationBaseId}/mark-picked-up -> { success }
|
|
13
|
+
// GET PickupNotifications/Contacts/{contactId}/notifications -> history[]
|
|
14
|
+
// GET PickupNotifications/Config -> config | 404
|
|
15
|
+
// POST PickupNotifications/Config -> create (409 if one already exists)
|
|
16
|
+
// PUT PickupNotifications/Config/{id} -> apply-if-present update
|
|
17
|
+
|
|
18
|
+
const BOLD = '\x1b[1m';
|
|
19
|
+
const DIM = '\x1b[2m';
|
|
20
|
+
const GRN = '\x1b[32m';
|
|
21
|
+
const RST = '\x1b[0m';
|
|
22
|
+
|
|
23
|
+
function fmtDate(d) {
|
|
24
|
+
return d ? new Date(d).toISOString().replace('T', ' ').slice(0, 19) : '-';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function notifyHandler(argv) {
|
|
28
|
+
// Field names must match NotifyContactRequest (Services/Messaging/IPickupNotificationService.cs)
|
|
29
|
+
// exactly -- "startReminderChain", NOT "remind". A mismatched name binds to default(false) and
|
|
30
|
+
// the reminder chain silently never starts.
|
|
31
|
+
const result = await messagingPost(`/PickupNotifications/Contacts/${argv.contactId}/notify`, {
|
|
32
|
+
message: argv.message,
|
|
33
|
+
startReminderChain: !!argv.remind,
|
|
34
|
+
cadenceHours: argv.cadenceHours,
|
|
35
|
+
maxReminders: argv.maxReminders,
|
|
36
|
+
});
|
|
37
|
+
console.log(`\n${GRN}✓${RST} Pickup notification sent to contact ${argv.contactId}.`);
|
|
38
|
+
console.log(` Conversation: ${result.conversationBaseId}`);
|
|
39
|
+
if (result.workflowJobId) console.log(` Reminder job: ${result.workflowJobId}`);
|
|
40
|
+
console.log('');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function markPickedUpHandler(argv) {
|
|
44
|
+
await messagingPost(`/PickupNotifications/Conversations/${argv.conversationId}/mark-picked-up`, {});
|
|
45
|
+
console.log(`\n${GRN}✓${RST} Conversation ${argv.conversationId} marked picked up. Reminders stopped.\n`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function historyHandler(argv) {
|
|
49
|
+
const history = await messagingGet(`/PickupNotifications/Contacts/${argv.contactId}/notifications`);
|
|
50
|
+
if (!Array.isArray(history) || !history.length) {
|
|
51
|
+
console.log(`\n No pickup notifications found for contact ${argv.contactId}.\n`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Fields must match PickupNotificationSendHistoryItem exactly -- the ONLY fields on that DTO
|
|
55
|
+
// are conversationBaseId, sentAtUtc, segmentCount, isInitialSend, workflowJobId. There is no
|
|
56
|
+
// id/message/pickedUp/type field; do not print ones that don't exist.
|
|
57
|
+
console.log(`\n${BOLD}Pickup notification history -- contact ${argv.contactId}${RST}\n`);
|
|
58
|
+
for (const h of history) {
|
|
59
|
+
const kind = h.isInitialSend ? `${GRN}initial${RST}` : 'reminder';
|
|
60
|
+
const chain = h.workflowJobId ? ` ${DIM}chain:${h.workflowJobId}${RST}` : '';
|
|
61
|
+
console.log(` ${fmtDate(h.sentAtUtc)} conv:${h.conversationBaseId ?? '-'} ${kind} ${h.segmentCount ?? 0} seg${chain}`);
|
|
62
|
+
}
|
|
63
|
+
console.log('');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function configGetHandler() {
|
|
67
|
+
let config;
|
|
68
|
+
try {
|
|
69
|
+
config = await messagingGet('/PickupNotifications/Config');
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (String(err.message).includes(' 404')) {
|
|
72
|
+
console.log(`\n No Pickup & Reminder Texting config exists for this site yet. Run "velaro pickup config set" to create one.\n`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
console.log(`\n${BOLD}Pickup & Reminder Texting config${RST} (id ${config.id})`);
|
|
78
|
+
console.log(` Enabled: ${config.isEnabled ? `${GRN}yes${RST}` : 'no'}`);
|
|
79
|
+
console.log(` Default cadence: ${config.defaultCadenceHours}h`);
|
|
80
|
+
console.log(` Default max reminders:${config.defaultMaxReminders}`);
|
|
81
|
+
console.log(` Message template: ${config.defaultMessageTemplate ?? '-'}`);
|
|
82
|
+
console.log(` Quiet hours override: ${config.quietHoursOverride ?? `${DIM}(account default)${RST}`}`);
|
|
83
|
+
console.log('');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function configSetHandler(argv) {
|
|
87
|
+
const body = {
|
|
88
|
+
isEnabled: argv.enabled,
|
|
89
|
+
defaultCadenceHours: argv.cadenceHours,
|
|
90
|
+
defaultMaxReminders: argv.maxReminders,
|
|
91
|
+
defaultMessageTemplate: argv.template,
|
|
92
|
+
quietHoursOverride: argv.quietHoursOverride,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
let existing = null;
|
|
96
|
+
try {
|
|
97
|
+
existing = await messagingGet('/PickupNotifications/Config');
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (!String(err.message).includes(' 404')) throw err;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const config = existing
|
|
103
|
+
? await messagingPut(`/PickupNotifications/Config/${existing.id}`, body)
|
|
104
|
+
: await messagingPost('/PickupNotifications/Config', body);
|
|
105
|
+
|
|
106
|
+
console.log(`\n${GRN}✓${RST} Pickup & Reminder Texting config ${existing ? 'updated' : 'created'} (id ${config.id}).\n`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const notifyCommand = {
|
|
110
|
+
command: 'notify <contactId>',
|
|
111
|
+
describe: 'Send a pickup notification text to a contact, optionally with recurring reminders',
|
|
112
|
+
builder: (y) => y
|
|
113
|
+
.positional('contactId', { describe: 'Contact ID to notify', type: 'number' })
|
|
114
|
+
.option('message', { describe: 'Notification text -- required; the initial send is always caller-supplied (the site template is only used for follow-up reminders)', type: 'string', demandOption: true })
|
|
115
|
+
.option('remind', { describe: 'Schedule recurring reminders until marked picked up', type: 'boolean', default: false })
|
|
116
|
+
.option('cadence-hours', { describe: 'Hours between reminders, 1-168 (default: site config default)', type: 'number' })
|
|
117
|
+
.option('max-reminders', { describe: 'Max reminder texts, 1-14 (default: site config default)', type: 'number' }),
|
|
118
|
+
handler: runCommand(notifyHandler),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const markPickedUpCommand = {
|
|
122
|
+
command: 'mark-picked-up <conversationId>',
|
|
123
|
+
describe: 'Mark a conversation as picked up -- stops any further reminder texts',
|
|
124
|
+
builder: (y) => y
|
|
125
|
+
.positional('conversationId', { describe: 'Conversation base ID', type: 'number' }),
|
|
126
|
+
handler: runCommand(markPickedUpHandler),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const historyCommand = {
|
|
130
|
+
command: 'history <contactId>',
|
|
131
|
+
describe: 'Show pickup notification history for a contact',
|
|
132
|
+
builder: (y) => y
|
|
133
|
+
.positional('contactId', { describe: 'Contact ID', type: 'number' }),
|
|
134
|
+
handler: runCommand(historyHandler),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const configCommand = {
|
|
138
|
+
command: 'config <subcommand>',
|
|
139
|
+
describe: 'Get or set this site\'s Pickup & Reminder Texting config',
|
|
140
|
+
builder: (y) => y
|
|
141
|
+
.command({
|
|
142
|
+
command: 'get',
|
|
143
|
+
describe: 'Show this site\'s Pickup & Reminder Texting config',
|
|
144
|
+
handler: runCommand(configGetHandler),
|
|
145
|
+
})
|
|
146
|
+
.command({
|
|
147
|
+
command: 'set',
|
|
148
|
+
describe: 'Create or update this site\'s Pickup & Reminder Texting config (apply-if-present -- omit a flag to leave it unchanged)',
|
|
149
|
+
builder: (y2) => y2
|
|
150
|
+
.option('enabled', { describe: 'Enable/disable pickup notifications site-wide', type: 'boolean' })
|
|
151
|
+
.option('cadence-hours', { describe: 'Default hours between reminders, 1-168', type: 'number' })
|
|
152
|
+
.option('max-reminders', { describe: 'Default max reminder texts, 1-14', type: 'number' })
|
|
153
|
+
.option('template', { describe: 'Default pickup notification message text', type: 'string' })
|
|
154
|
+
.option('quiet-hours-override', { describe: 'Quiet-hours override (pass "" to clear back to account default)', type: 'string' }),
|
|
155
|
+
handler: runCommand(configSetHandler),
|
|
156
|
+
})
|
|
157
|
+
.demandCommand(1, 'Specify a config subcommand: get | set'),
|
|
158
|
+
handler: () => {},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const pickupCommand = {
|
|
162
|
+
command: 'pickup <subcommand>',
|
|
163
|
+
describe: 'Pickup & Reminder Texting -- notify contacts, manage reminders, and site config',
|
|
164
|
+
builder: (y) => y
|
|
165
|
+
.command(notifyCommand)
|
|
166
|
+
.command(markPickedUpCommand)
|
|
167
|
+
.command(historyCommand)
|
|
168
|
+
.command(configCommand)
|
|
169
|
+
.demandCommand(1, 'Specify a pickup subcommand: notify | mark-picked-up | history | config')
|
|
170
|
+
.strict(),
|
|
171
|
+
handler: () => {},
|
|
172
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// velaro pricing — view and manage the pricing catalog (superadmin only)
|
|
2
|
+
import { request } from '../api.js';
|
|
3
|
+
|
|
4
|
+
const TYPE_ORDER = ['plan', 'addon', 'block', 'seat', 'compliance', 'skill'];
|
|
5
|
+
|
|
6
|
+
function fmt(pkg) {
|
|
7
|
+
const price = pkg.contactSales
|
|
8
|
+
? 'Contact sales'
|
|
9
|
+
: pkg.monthlyPriceCents
|
|
10
|
+
? `$${(pkg.monthlyPriceCents / 100).toFixed(0)}${pkg.unitLabel || '/mo'}`
|
|
11
|
+
: 'Free';
|
|
12
|
+
return ` ${pkg.packageKey.padEnd(42)} ${price.padEnd(18)} v${pkg.version} ${pkg.displayName}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function listPackages(args) {
|
|
16
|
+
const pkgs = await request('GET', '/Pricing/catalog');
|
|
17
|
+
if (!Array.isArray(pkgs) || !pkgs.length) { console.log('No packages found.'); return; }
|
|
18
|
+
|
|
19
|
+
const typeFilter = args.type?.toLowerCase();
|
|
20
|
+
const filtered = typeFilter ? pkgs.filter(p => p.packageType === typeFilter) : pkgs;
|
|
21
|
+
if (!filtered.length) { console.log(`No packages with type '${typeFilter}'.`); return; }
|
|
22
|
+
|
|
23
|
+
const byType = {};
|
|
24
|
+
for (const p of filtered) {
|
|
25
|
+
if (!byType[p.packageType]) byType[p.packageType] = [];
|
|
26
|
+
byType[p.packageType].push(p);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(`\nPricing Catalog (${filtered.length} packages)\n`);
|
|
30
|
+
const types = TYPE_ORDER.filter(t => byType[t]);
|
|
31
|
+
for (const t of types) {
|
|
32
|
+
console.log(`── ${t.toUpperCase()} (${byType[t].length}) ──`);
|
|
33
|
+
byType[t].forEach(p => console.log(fmt(p)));
|
|
34
|
+
console.log('');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getPackage(args) {
|
|
39
|
+
if (!args.key) { console.error('Usage: velaro pricing get --key addon.appointments'); process.exit(1); }
|
|
40
|
+
const p = await request('GET', `/Pricing/catalog/${args.key}`);
|
|
41
|
+
console.log(JSON.stringify(p, null, 2));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function updatePackage(args) {
|
|
45
|
+
if (!args.key) { console.error('Usage: velaro pricing update --key addon.appointments [--price 59] [--name "..."] [--contact-sales]'); process.exit(1); }
|
|
46
|
+
const payload = {};
|
|
47
|
+
if (args.price != null) payload.monthlyPriceCents = Math.round(parseFloat(args.price) * 100);
|
|
48
|
+
if (args.discount != null) payload.annualDiscountBps = Math.round(parseFloat(args.discount) * 100);
|
|
49
|
+
if (args.name) payload.displayName = args.name;
|
|
50
|
+
if (args.description) payload.description = args.description;
|
|
51
|
+
if (args['contact-sales'] != null) payload.contactSales = args['contact-sales'];
|
|
52
|
+
if (args.active != null) payload.isActive = args.active;
|
|
53
|
+
|
|
54
|
+
if (!Object.keys(payload).length) { console.error('No fields to update. Use --price, --name, --description, --contact-sales, --active.'); process.exit(1); }
|
|
55
|
+
|
|
56
|
+
const result = await request('PUT', `/Pricing/catalog/${args.key}`, payload);
|
|
57
|
+
console.log(`✅ Package '${result.packageKey}' updated to v${result.version}.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function createPackage(args) {
|
|
61
|
+
if (!args.key || !args.type || !args.name) {
|
|
62
|
+
console.error('Usage: velaro pricing create --key addon.my-feature --type addon --name "My Feature" [--price 49] [--description "..."] [--contact-sales]');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
const body = {
|
|
66
|
+
packageKey: args.key,
|
|
67
|
+
packageType: args.type,
|
|
68
|
+
displayName: args.name,
|
|
69
|
+
description: args.description,
|
|
70
|
+
monthlyPriceCents: args.price ? Math.round(parseFloat(args.price) * 100) : 0,
|
|
71
|
+
contactSales: args['contact-sales'] ?? false,
|
|
72
|
+
unitLabel: args.unit || '/mo',
|
|
73
|
+
};
|
|
74
|
+
const result = await request('POST', '/Pricing/catalog', body);
|
|
75
|
+
console.log(`✅ Created package '${result.packageKey}' (v${result.version}).`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function seedCatalog() {
|
|
79
|
+
const result = await request('POST', '/Pricing/catalog/seed', {});
|
|
80
|
+
console.log(`✅ Seeded ${result.seeded} packages from PricingCatalogSeeder.cs defaults.`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function bulkAdjust(args) {
|
|
84
|
+
if (args.percent == null) { console.error('Usage: velaro pricing bulk-adjust --percent 10 [--type addon] [--force-contact-sales]'); process.exit(1); }
|
|
85
|
+
const pct = parseFloat(args.percent);
|
|
86
|
+
if (isNaN(pct) || pct === 0) { console.error('--percent must be a non-zero number (e.g. 10 for +10%, -5 for -5%)'); process.exit(1); }
|
|
87
|
+
|
|
88
|
+
const body = { percentChange: pct };
|
|
89
|
+
if (args.type) body.packageType = args.type;
|
|
90
|
+
if (args['force-contact-sales']) body.forceContactSales = true;
|
|
91
|
+
|
|
92
|
+
const result = await request('POST', '/Pricing/catalog/bulk-adjust', body);
|
|
93
|
+
const sign = pct > 0 ? '+' : '';
|
|
94
|
+
console.log(`\n✅ Applied ${sign}${pct}% to ${result.updated} packages:\n`);
|
|
95
|
+
for (const c of result.changes) {
|
|
96
|
+
const oldFmt = `$${(c.oldPrice / 100).toFixed(0)}`;
|
|
97
|
+
const newFmt = `$${(c.newPrice / 100).toFixed(0)}`;
|
|
98
|
+
const chgFmt = c.change >= 0 ? `+$${(c.change / 100).toFixed(0)}` : `-$${(Math.abs(c.change) / 100).toFixed(0)}`;
|
|
99
|
+
console.log(` ${c.packageKey.padEnd(42)} ${oldFmt.padStart(8)} → ${newFmt.padStart(8)} (${chgFmt}) ${c.displayName}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function pricingCommand(yargs) {
|
|
104
|
+
return yargs
|
|
105
|
+
.command('list', 'List all pricing packages', y => y.option('type', { type: 'string', desc: 'Filter: plan | addon | block | seat | compliance | skill' }), a => listPackages(a))
|
|
106
|
+
.command('get', 'Get a single package by key', y => y.option('key', { type: 'string', demandOption: true }), a => getPackage(a))
|
|
107
|
+
.command('update', 'Update a package price or metadata', y => y
|
|
108
|
+
.option('key', { type: 'string', demandOption: true, desc: 'Package key' })
|
|
109
|
+
.option('price', { type: 'number', desc: 'New monthly price in dollars (e.g. 59)' })
|
|
110
|
+
.option('discount', { type: 'number', desc: 'Annual discount percentage (e.g. 20)' })
|
|
111
|
+
.option('name', { type: 'string', desc: 'New display name' })
|
|
112
|
+
.option('description', { type: 'string', desc: 'New description' })
|
|
113
|
+
.option('contact-sales', { type: 'boolean', desc: 'Set to hide price and show "Contact sales"' })
|
|
114
|
+
.option('active', { type: 'boolean', desc: 'Set to false to deactivate' })
|
|
115
|
+
, a => updatePackage(a))
|
|
116
|
+
.command('create', 'Create a new pricing package', y => y
|
|
117
|
+
.option('key', { type: 'string', demandOption: true, desc: 'Unique key (e.g. addon.my-feature)' })
|
|
118
|
+
.option('type', { type: 'string', demandOption: true, desc: 'plan | addon | block | seat | compliance | skill' })
|
|
119
|
+
.option('name', { type: 'string', demandOption: true, desc: 'Display name' })
|
|
120
|
+
.option('price', { type: 'number', desc: 'Monthly price in dollars' })
|
|
121
|
+
.option('description', { type: 'string', desc: 'Short description' })
|
|
122
|
+
.option('unit', { type: 'string', desc: 'Unit label (default: /mo)' })
|
|
123
|
+
.option('contact-sales', { type: 'boolean', desc: 'Show "Contact sales" instead of price' })
|
|
124
|
+
, a => createPackage(a))
|
|
125
|
+
.command('bulk-adjust', 'Apply a blanket % price change across all or filtered packages', y => y
|
|
126
|
+
.option('percent', { type: 'number', demandOption: true, desc: 'Percentage change: 10 = +10%, -5 = -5%' })
|
|
127
|
+
.option('type', { type: 'string', desc: 'Limit to one type: plan | addon | block | seat | compliance' })
|
|
128
|
+
.option('force-contact-sales', { type: 'boolean', desc: 'Also adjust "Contact sales" packages (default: skip them)' })
|
|
129
|
+
, a => bulkAdjust(a))
|
|
130
|
+
.command('seed', 'Re-seed catalog from PricingCatalogSeeder.cs defaults', () => {}, () => seedCatalog())
|
|
131
|
+
.demandCommand(1, 'Specify a subcommand: list | get | update | create | bulk-adjust | seed');
|
|
132
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { get } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
function bar(used, total, width = 20) {
|
|
5
|
+
if (!total) return '[ ] n/a';
|
|
6
|
+
const pct = Math.min(used / total, 1);
|
|
7
|
+
const filled = Math.round(pct * width);
|
|
8
|
+
const color = pct >= 0.9 ? '\x1b[31m' : pct >= 0.7 ? '\x1b[33m' : '\x1b[32m';
|
|
9
|
+
return `${color}[${'█'.repeat(filled)}${' '.repeat(width - filled)}]\x1b[0m ${(pct * 100).toFixed(0)}%`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ── sync-status ────────────────────────────────────────────────────────────
|
|
13
|
+
// GET /ProductCatalog/SyncStatus — per-platform e-commerce product ingestion
|
|
14
|
+
// status (last sync time, products indexed, cap usage). Site is resolved
|
|
15
|
+
// server-side from the caller's auth context — never a client-supplied value.
|
|
16
|
+
|
|
17
|
+
const syncStatusCommand = {
|
|
18
|
+
command: 'sync-status',
|
|
19
|
+
describe: 'Show e-commerce product sync status (Shopify/BigCommerce/WooCommerce/Magento) — last sync time, products indexed, cap usage',
|
|
20
|
+
handler: runCommand(async () => {
|
|
21
|
+
const data = await get('/ProductCatalog/SyncStatus');
|
|
22
|
+
|
|
23
|
+
console.log('\n\x1b[1mProduct Sync Status\x1b[0m');
|
|
24
|
+
|
|
25
|
+
if (data.maxProductsIndexed > 0) {
|
|
26
|
+
const used = data.platforms.reduce((s, p) => s + (p.productsIndexed ?? 0), 0);
|
|
27
|
+
console.log(`Cap usage: ${bar(used, data.maxProductsIndexed)} ${used.toLocaleString()} / ${data.maxProductsIndexed.toLocaleString()} products`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log('');
|
|
31
|
+
for (const p of data.platforms) {
|
|
32
|
+
const name = p.platform.charAt(0).toUpperCase() + p.platform.slice(1);
|
|
33
|
+
if (!p.ingestionAvailable) {
|
|
34
|
+
console.log(` ${name.padEnd(14)} \x1b[90mno nightly ingestion yet — always live lookup\x1b[0m`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const configured = p.configured === null ? '' : p.configured ? '\x1b[32mconnected\x1b[0m' : '\x1b[90mnot connected\x1b[0m';
|
|
38
|
+
const errBadge = p.hasErrors ? ' \x1b[31m⚠ has errors\x1b[0m' : '';
|
|
39
|
+
const lastSync = p.lastSyncAt ? new Date(p.lastSyncAt).toLocaleString() : 'never';
|
|
40
|
+
console.log(` ${name.padEnd(14)} ${configured}`);
|
|
41
|
+
console.log(` Last sync: ${lastSync} Products indexed: ${p.productsIndexed.toLocaleString()}${errBadge}`);
|
|
42
|
+
}
|
|
43
|
+
console.log('\nNote: price and availability shown to customers are always fetched live — never from this index.\n');
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const productCommand = {
|
|
48
|
+
command: 'product <subcommand>',
|
|
49
|
+
describe: 'E-commerce product catalog sync status',
|
|
50
|
+
builder: (yargs) =>
|
|
51
|
+
yargs
|
|
52
|
+
.command(syncStatusCommand)
|
|
53
|
+
.demandCommand(1, 'Specify a subcommand: sync-status'),
|
|
54
|
+
handler: () => {},
|
|
55
|
+
};
|