@series-a/cli 1.3.0 → 1.3.2
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/CHANGELOG.md +184 -51
- package/LICENSE +7 -0
- package/README.md +249 -115
- package/SKILLS.md +359 -0
- package/bin/advisoros.mjs +2 -0
- package/package.json +50 -15
- package/src/api.mjs +102 -0
- package/src/commands/auth.mjs +193 -0
- package/src/commands/brand.mjs +439 -0
- package/src/commands/email.mjs +130 -0
- package/src/commands/flow.mjs +175 -0
- package/src/commands/insights.mjs +1813 -0
- package/src/commands/knowledge-sync.mjs +519 -0
- package/src/commands/resources.mjs +2214 -0
- package/src/commands/strategies.mjs +242 -0
- package/src/commands/voice.mjs +212 -0
- package/src/config.mjs +82 -0
- package/src/index.mjs +79 -0
- package/src/ui.mjs +104 -0
- package/bin/setu.mjs +0 -797
- package/lib/api.mjs +0 -84
- package/lib/config.mjs +0 -42
- package/lib/ui.mjs +0 -68
package/bin/setu.mjs
DELETED
|
@@ -1,797 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* setu — internal CLI for building claimable practice and dentist profiles.
|
|
4
|
-
*
|
|
5
|
-
* setu login
|
|
6
|
-
* setu practice create --url https://smiledental.co.uk
|
|
7
|
-
* setu practice create --name "Smile Dental" --town Portsmouth
|
|
8
|
-
* setu practice import practices.csv
|
|
9
|
-
* setu practice review <draft-id>
|
|
10
|
-
* setu practice publish <draft-id>
|
|
11
|
-
* setu practice claim-link <practice-id>
|
|
12
|
-
* setu dentist create --practice <id> --name "Dr Jane Roe"
|
|
13
|
-
* setu status [<practice-id>]
|
|
14
|
-
*
|
|
15
|
-
* Global flags: --json (machine-readable output), --operator <name>
|
|
16
|
-
*/
|
|
17
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
18
|
-
import { fileURLToPath } from "node:url";
|
|
19
|
-
import { call } from "../lib/api.mjs";
|
|
20
|
-
import { loadConfig, saveConfig, clearConfig, configPath, DEFAULT_API } from "../lib/config.mjs";
|
|
21
|
-
|
|
22
|
-
const VERSION = JSON.parse(
|
|
23
|
-
readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"),
|
|
24
|
-
).version;
|
|
25
|
-
import { ask, confirm, log, ok, fail, warn, heading, table, spinner, c } from "../lib/ui.mjs";
|
|
26
|
-
|
|
27
|
-
function parseArgs(argv) {
|
|
28
|
-
const positional = [];
|
|
29
|
-
const flags = {};
|
|
30
|
-
for (let i = 0; i < argv.length; i++) {
|
|
31
|
-
const a = argv[i];
|
|
32
|
-
if (a.startsWith("--")) {
|
|
33
|
-
const key = a.slice(2);
|
|
34
|
-
const next = argv[i + 1];
|
|
35
|
-
let value = true;
|
|
36
|
-
if (next && !next.startsWith("--")) { value = next; i++; }
|
|
37
|
-
// Repeatable flags (--set, --add, --remove) collect into arrays.
|
|
38
|
-
if (key in flags) {
|
|
39
|
-
flags[key] = Array.isArray(flags[key]) ? [...flags[key], value] : [flags[key], value];
|
|
40
|
-
} else flags[key] = value;
|
|
41
|
-
} else positional.push(a);
|
|
42
|
-
}
|
|
43
|
-
return { positional, flags };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
47
|
-
const JSON_OUT = !!flags.json;
|
|
48
|
-
const out = (obj) => { if (JSON_OUT) log(JSON.stringify(obj, null, 2)); };
|
|
49
|
-
|
|
50
|
-
function usage() {
|
|
51
|
-
log(`
|
|
52
|
-
${c.bold("setu")} — build claimable Setu profiles
|
|
53
|
-
|
|
54
|
-
${c.bold("Commands")}
|
|
55
|
-
login Store your internal CLI key
|
|
56
|
-
logout Remove the stored key from this machine
|
|
57
|
-
whoami Show the signed-in operator
|
|
58
|
-
practice create Build a profile draft with AI
|
|
59
|
-
--url <website> from a website, or
|
|
60
|
-
--name <name> --town <town> from a name + town (found on Google)
|
|
61
|
-
[--pricing <url>] [--booking <url>] [--email <contact email>]
|
|
62
|
-
practice review <draft-id> Show everything the AI extracted
|
|
63
|
-
practice publish <draft-id> Publish as an unclaimed listing
|
|
64
|
-
[--no-dentists] don't seed detected clinicians
|
|
65
|
-
practice import <file.csv> Bulk build + optionally publish
|
|
66
|
-
[--publish] [--concurrency 2] [--dry-run]
|
|
67
|
-
[--out results.json] [--resume results.json] [--limit N]
|
|
68
|
-
practice claim-link <practice-id> Outreach link for the owner
|
|
69
|
-
[--rotate]
|
|
70
|
-
practice delete <practice-id> Remove an unclaimed seeded profile
|
|
71
|
-
[--unpublish] [--yes]
|
|
72
|
-
practice update <practice-id> Edit fields on a seeded profile
|
|
73
|
-
--set field=value repeatable, e.g. --set tagline="Gentle care"
|
|
74
|
-
--set-enrichment path=value e.g. --set-enrichment amenities="Wi-Fi|Parking"
|
|
75
|
-
[--booking <url>] [--website <url>] shortcuts for the booking / website links
|
|
76
|
-
[--fields] list the fields you can set
|
|
77
|
-
practice images <practice-id> Manage photos on a seeded profile
|
|
78
|
-
[--add <url>] [--remove <url|index>] [--order "url1,url2"]
|
|
79
|
-
[--logo <url>] [--replace "url1,url2"] [--clear] [--list]
|
|
80
|
-
[--upload <file|url>] [--upload-logo <file|url>]
|
|
81
|
-
practice pricing <practice-id> Manage treatment prices
|
|
82
|
-
[--set "Implant=1200"] [--remove "Implant"] [--clear] [--list]
|
|
83
|
-
practice list List seeded practices
|
|
84
|
-
[--limit 50] [--search <name>]
|
|
85
|
-
dentist create --name "Dr Jane Roe" Seed one clinician
|
|
86
|
-
[--practice <id>] [--profile-url <url>] [--town <town>]
|
|
87
|
-
[--photo <url>] [--gallery "url1,url2"] [--phone <n>]
|
|
88
|
-
[--website <url>] [--booking <url>] [--bio "..."]
|
|
89
|
-
dentist list List seeded clinicians
|
|
90
|
-
[--limit 50] [--search <name>] [--practice <id>]
|
|
91
|
-
dentist update <dentist-id> Edit fields on a seeded clinician
|
|
92
|
-
dentist images <dentist-id> Manage a clinician's photos
|
|
93
|
-
dentist pricing <dentist-id> Manage a clinician's treatment prices
|
|
94
|
-
dentist delete <dentist-id> Remove an unclaimed seeded clinician
|
|
95
|
-
[--unpublish] [--yes]
|
|
96
|
-
status [<profile-id>] List or inspect seeded profiles
|
|
97
|
-
[--dentist] [--full] [--limit 50] [--search <name>]
|
|
98
|
-
claims Review dashboard: pending claims + ready profiles
|
|
99
|
-
[--status pending|approved|rejected|all] [--dentist] [--ready] [--limit 50]
|
|
100
|
-
claims show <claim-id> Full detail for one claim
|
|
101
|
-
claims approve <claim-id> Hand the profile to the claimant
|
|
102
|
-
claims decline <claim-id> Decline a claim [--reason "..."]
|
|
103
|
-
|
|
104
|
-
${c.bold("Global")} --json --yes --verbose --operator <name> --version
|
|
105
|
-
`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function cmdLogin() {
|
|
109
|
-
const cfg = loadConfig();
|
|
110
|
-
const api = (await ask(`API base ${c.dim(`[${cfg.api || DEFAULT_API}]`)}: `)) || cfg.api || DEFAULT_API;
|
|
111
|
-
const key = await ask("Internal CLI key: ", { silent: true });
|
|
112
|
-
const operator = (await ask(`Your name (for attribution) ${c.dim(`[${cfg.operator || ""}]`)}: `)) || cfg.operator || "cli";
|
|
113
|
-
const res = await call("cli-auth", {}, { api, key, operator });
|
|
114
|
-
saveConfig({ api, key, operator });
|
|
115
|
-
ok(`Signed in as ${c.bold(operator)} — ${res.site}`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function cmdLogout() {
|
|
119
|
-
clearConfig();
|
|
120
|
-
ok(`Signed out — removed ${configPath()}`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function cmdPracticeDelete(id, type = "practice") {
|
|
124
|
-
if (!id) { fail("Usage: setu practice delete <practice-id> [--unpublish]"); process.exit(1); }
|
|
125
|
-
const unpublishOnly = !!flags.unpublish;
|
|
126
|
-
if (!JSON_OUT && !flags.yes) {
|
|
127
|
-
const proceed = await confirm(
|
|
128
|
-
unpublishOnly
|
|
129
|
-
? "Hide this seeded profile from patients?"
|
|
130
|
-
: "Permanently delete this unclaimed seeded profile (and its seeded clinicians)?",
|
|
131
|
-
);
|
|
132
|
-
if (!proceed) { warn("Cancelled."); return; }
|
|
133
|
-
}
|
|
134
|
-
const res = await call("cli-profile-delete", {
|
|
135
|
-
id,
|
|
136
|
-
profile_type: type === "dentist" || flags.dentist ? "dentist" : "practice",
|
|
137
|
-
unpublish_only: unpublishOnly,
|
|
138
|
-
});
|
|
139
|
-
if (JSON_OUT) return out(res);
|
|
140
|
-
ok(`${res.name} — ${res.action}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const asList = (v) => (v === undefined || v === true ? [] : Array.isArray(v) ? v : [v]);
|
|
144
|
-
const splitList = (v) =>
|
|
145
|
-
asList(v).flatMap((item) => String(item).split(",").map((s) => s.trim()).filter(Boolean));
|
|
146
|
-
|
|
147
|
-
async function cmdProfileUpdate(type, id) {
|
|
148
|
-
if (flags.fields) {
|
|
149
|
-
// The server answers with the whitelist when nothing valid is sent.
|
|
150
|
-
try {
|
|
151
|
-
const r = await call("cli-profile-update", { id: id || "00000000-0000-0000-0000-000000000000", profile_type: type, read: true });
|
|
152
|
-
if (JSON_OUT) return out({ fields: r.allowed_fields, enrichment_paths: r.allowed_enrichment_paths ?? [] });
|
|
153
|
-
heading(`Fields you can set on a ${type}`);
|
|
154
|
-
log(" " + r.allowed_fields.join(", "));
|
|
155
|
-
if (r.allowed_enrichment_paths?.length) {
|
|
156
|
-
heading("Extra fields (use --set-enrichment path=value)");
|
|
157
|
-
log(" " + r.allowed_enrichment_paths.join(", "));
|
|
158
|
-
}
|
|
159
|
-
return;
|
|
160
|
-
} catch (e) {
|
|
161
|
-
const fields = e.data?.allowed_fields;
|
|
162
|
-
if (fields) {
|
|
163
|
-
if (JSON_OUT) return out({ fields });
|
|
164
|
-
heading(`Fields you can set on a ${type}`);
|
|
165
|
-
log(" " + fields.join(", "));
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
throw e;
|
|
169
|
-
}
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
if (!id) { fail(`Usage: setu ${type} update <id> [--set field=value] [--booking <url>] [--website <url>]`); process.exit(1); }
|
|
173
|
-
|
|
174
|
-
const set = {};
|
|
175
|
-
const pairs = asList(flags.set);
|
|
176
|
-
for (const pair of pairs) {
|
|
177
|
-
const raw = String(pair);
|
|
178
|
-
const eq = raw.indexOf("=");
|
|
179
|
-
if (eq < 1) { fail(`Bad --set value: "${raw}". Use field=value.`); process.exit(1); }
|
|
180
|
-
set[raw.slice(0, eq).trim()] = raw.slice(eq + 1);
|
|
181
|
-
}
|
|
182
|
-
const enrichmentSet = {};
|
|
183
|
-
for (const pair of asList(flags["set-enrichment"])) {
|
|
184
|
-
const raw = String(pair);
|
|
185
|
-
const eq = raw.indexOf("=");
|
|
186
|
-
if (eq < 1) { fail(`Bad --set-enrichment value: "${raw}". Use path=value.`); process.exit(1); }
|
|
187
|
-
enrichmentSet[raw.slice(0, eq).trim()] = raw.slice(eq + 1);
|
|
188
|
-
}
|
|
189
|
-
if (flags.booking !== undefined) set.booking_url = flags.booking === true ? "" : String(flags.booking);
|
|
190
|
-
if (flags.website !== undefined) set[type === "dentist" ? "website_url" : "website"] = flags.website === true ? "" : String(flags.website);
|
|
191
|
-
if (!Object.keys(set).length && !Object.keys(enrichmentSet).length) { fail("Pass at least one --set field=value, --set-enrichment path=value, --booking <url> or --website <url> (or --fields to see what you can set)."); process.exit(1); }
|
|
192
|
-
|
|
193
|
-
const res = await call("cli-profile-update", {
|
|
194
|
-
id,
|
|
195
|
-
profile_type: type,
|
|
196
|
-
set,
|
|
197
|
-
...(Object.keys(enrichmentSet).length ? { enrichment_set: enrichmentSet } : {}),
|
|
198
|
-
});
|
|
199
|
-
if (JSON_OUT) return out(res);
|
|
200
|
-
ok(`${res.name} — updated ${res.updated.join(", ")}`);
|
|
201
|
-
if (res.rejected?.length) warn(`Ignored unknown field(s): ${res.rejected.join(", ")}`);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const MIME_BY_EXT = {
|
|
205
|
-
jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png",
|
|
206
|
-
webp: "image/webp", svg: "image/svg+xml", gif: "image/gif",
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
/** A local path becomes base64; a URL is fetched server-side. */
|
|
210
|
-
function uploadPayload(src) {
|
|
211
|
-
const value = String(src);
|
|
212
|
-
if (/^https?:\/\//i.test(value)) return { source_url: value };
|
|
213
|
-
const ext = value.split(".").pop().toLowerCase();
|
|
214
|
-
return {
|
|
215
|
-
data: readFileSync(value).toString("base64"),
|
|
216
|
-
content_type: MIME_BY_EXT[ext] || "image/jpeg",
|
|
217
|
-
filename: value.split(/[\\/]/).pop(),
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async function uploadImages(type, id, sources, attach) {
|
|
222
|
-
const urls = [];
|
|
223
|
-
for (const src of sources) {
|
|
224
|
-
const s = spinner(`Uploading ${src}`);
|
|
225
|
-
try {
|
|
226
|
-
const res = await call("cli-profile-upload", { id, profile_type: type, attach, ...uploadPayload(src) });
|
|
227
|
-
s.stop();
|
|
228
|
-
urls.push(res.url);
|
|
229
|
-
if (!JSON_OUT) ok(`${attach === "logo" ? "Logo" : "Photo"} uploaded — ${c.dim(res.url)}`);
|
|
230
|
-
} catch (e) { s.stop(); throw e; }
|
|
231
|
-
}
|
|
232
|
-
return urls;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async function cmdProfileImages(type, id) {
|
|
236
|
-
if (!id) { fail(`Usage: setu ${type} images <id> [--add <url>] [--upload <file>] [--remove <url|index>] [--order "a,b"] [--logo <url>] [--upload-logo <file>]`); process.exit(1); }
|
|
237
|
-
|
|
238
|
-
const uploads = splitList(flags.upload);
|
|
239
|
-
const logoUploads = splitList(flags["upload-logo"]);
|
|
240
|
-
const uploaded = [];
|
|
241
|
-
if (uploads.length) uploaded.push(...await uploadImages(type, id, uploads, "gallery"));
|
|
242
|
-
if (logoUploads.length) await uploadImages(type, id, logoUploads.slice(0, 1), "logo");
|
|
243
|
-
|
|
244
|
-
const images = {};
|
|
245
|
-
if (splitList(flags.add).length) images.add = splitList(flags.add);
|
|
246
|
-
if (splitList(flags.remove).length) images.remove = splitList(flags.remove);
|
|
247
|
-
if (splitList(flags.order).length) images.order = splitList(flags.order);
|
|
248
|
-
if (splitList(flags.replace).length) images.replace = splitList(flags.replace);
|
|
249
|
-
if (flags.clear === true) images.clear = true;
|
|
250
|
-
if (flags.logo !== undefined) images.logo = flags.logo === true ? null : String(flags.logo);
|
|
251
|
-
|
|
252
|
-
if (!Object.keys(images).length || flags.list) {
|
|
253
|
-
// Listing is a no-op update: ask the server for the current state.
|
|
254
|
-
const res = await call("cli-profile-update", { id, profile_type: type, read: true });
|
|
255
|
-
if (JSON_OUT) return out(res);
|
|
256
|
-
heading(`${res.name} — photos (${res.gallery.length})`);
|
|
257
|
-
res.gallery.forEach((url, i) => log(` ${String(i).padEnd(3)} ${url}`));
|
|
258
|
-
log(` ${c.dim(type === "dentist" ? "main photo" : "logo")}: ${res.logo ?? c.dim("none")}`);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (images.clear && !JSON_OUT && !flags.yes) {
|
|
263
|
-
const proceed = await confirm("Remove every photo from this profile?");
|
|
264
|
-
if (!proceed) { warn("Cancelled."); return; }
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const res = await call("cli-profile-update", { id, profile_type: type, images });
|
|
268
|
-
if (JSON_OUT) return out(res);
|
|
269
|
-
ok(`${res.name} — ${res.gallery.length} photo(s)${res.logo ? ", logo set" : ""}`);
|
|
270
|
-
res.gallery.forEach((url, i) => log(` ${String(i).padEnd(3)} ${c.dim(url)}`));
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
async function cmdProfilePricing(type, id) {
|
|
274
|
-
if (!id) { fail(`Usage: setu ${type} pricing <id> [--set "Implant=1200"] [--remove "Implant"] [--clear] [--list]`); process.exit(1); }
|
|
275
|
-
|
|
276
|
-
const setPairs = asList(flags.set);
|
|
277
|
-
const removals = splitList(flags.remove);
|
|
278
|
-
const wantsWrite = setPairs.length || removals.length || flags.clear === true;
|
|
279
|
-
|
|
280
|
-
if (!wantsWrite || flags.list) {
|
|
281
|
-
const res = await call("cli-profile-update", { id, profile_type: type, read: true });
|
|
282
|
-
if (JSON_OUT) return out({ id, pricing: res.pricing ?? [] });
|
|
283
|
-
const rows = res.pricing ?? [];
|
|
284
|
-
heading(`${res.name} — prices (${rows.length})`);
|
|
285
|
-
if (!rows.length) log(c.dim(" none yet — add with --set \"Implant=1200\""));
|
|
286
|
-
rows.forEach((r) => {
|
|
287
|
-
const range = r.amount_to ? `${r.amount}–${r.amount_to}` : r.amount;
|
|
288
|
-
log(` ${String(r.label).padEnd(34)} £${range ?? "?"}`);
|
|
289
|
-
});
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const pricing = {};
|
|
294
|
-
if (setPairs.length) {
|
|
295
|
-
pricing.set = {};
|
|
296
|
-
for (const pair of setPairs) {
|
|
297
|
-
const raw = String(pair);
|
|
298
|
-
const eq = raw.indexOf("=");
|
|
299
|
-
if (eq < 1) { fail(`Bad --set value: "${raw}". Use "Treatment=amount" (or "Treatment=800-1200").`); process.exit(1); }
|
|
300
|
-
pricing.set[raw.slice(0, eq).trim()] = raw.slice(eq + 1).trim();
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
if (removals.length) pricing.remove = removals;
|
|
304
|
-
if (flags.clear === true) {
|
|
305
|
-
if (!JSON_OUT && !flags.yes) {
|
|
306
|
-
const proceed = await confirm("Remove every price from this profile?");
|
|
307
|
-
if (!proceed) { warn("Cancelled."); return; }
|
|
308
|
-
}
|
|
309
|
-
pricing.clear = true;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const res = await call("cli-profile-update", { id, profile_type: type, pricing });
|
|
313
|
-
if (JSON_OUT) return out(res);
|
|
314
|
-
const rows = res.pricing ?? [];
|
|
315
|
-
ok(`${res.name} — ${rows.length} price(s) on file`);
|
|
316
|
-
rows.forEach((r) => log(` ${String(r.label).padEnd(34)} ${c.dim(`£${r.amount_to ? `${r.amount}–${r.amount_to}` : r.amount}`)}`));
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
async function cmdWhoami() {
|
|
320
|
-
const res = await call("cli-auth", {});
|
|
321
|
-
const cfg = loadConfig();
|
|
322
|
-
if (JSON_OUT) return out({ ...res, api: cfg.api });
|
|
323
|
-
table([["Operator", res.operator], ["Site", res.site], ["API", cfg.api]]);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function summarise(res) {
|
|
327
|
-
table([
|
|
328
|
-
["Draft", res.draft_id],
|
|
329
|
-
["Name", res.name],
|
|
330
|
-
["Status", res.status],
|
|
331
|
-
["Readiness", `${res.readiness_score}/100`],
|
|
332
|
-
["Services", res.services],
|
|
333
|
-
["Priced items", res.pricing_rows],
|
|
334
|
-
["Clinicians found", res.dentists_found],
|
|
335
|
-
["Google reviews", res.google_reviews],
|
|
336
|
-
]);
|
|
337
|
-
if (Array.isArray(res.missing) && res.missing.length) {
|
|
338
|
-
warn(`Missing: ${res.missing.slice(0, 6).map((m) => (typeof m === "string" ? m : m.field ?? "")).join(", ")}`);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
async function cmdPracticeCreate() {
|
|
343
|
-
const payload = {
|
|
344
|
-
website: typeof flags.url === "string" ? flags.url : undefined,
|
|
345
|
-
name: typeof flags.name === "string" ? flags.name : undefined,
|
|
346
|
-
town: typeof flags.town === "string" ? flags.town : undefined,
|
|
347
|
-
postcode: typeof flags.postcode === "string" ? flags.postcode : undefined,
|
|
348
|
-
contact_email: typeof flags.email === "string" ? flags.email : undefined,
|
|
349
|
-
booking_url: typeof flags.booking === "string" ? flags.booking : undefined,
|
|
350
|
-
pricing_urls: typeof flags.pricing === "string" ? [flags.pricing] : undefined,
|
|
351
|
-
};
|
|
352
|
-
if (!payload.website && !payload.name) {
|
|
353
|
-
fail("Pass --url, or --name with --town.");
|
|
354
|
-
process.exit(1);
|
|
355
|
-
}
|
|
356
|
-
const s = spinner("Finding and reading the practice online");
|
|
357
|
-
try {
|
|
358
|
-
const res = await call("cli-profile-create", payload);
|
|
359
|
-
s.stop();
|
|
360
|
-
if (JSON_OUT) return out(res);
|
|
361
|
-
heading("Draft built");
|
|
362
|
-
summarise(res);
|
|
363
|
-
log(`\n${c.dim("Next:")} setu practice publish ${res.draft_id}`);
|
|
364
|
-
} catch (e) {
|
|
365
|
-
s.stop();
|
|
366
|
-
throw e;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
async function cmdPracticeReview(draftId) {
|
|
371
|
-
if (!draftId) { fail("Usage: setu practice review <draft-id>"); process.exit(1); }
|
|
372
|
-
const res = await call("cli-profile-review", { draft_id: draftId });
|
|
373
|
-
if (JSON_OUT) return out(res);
|
|
374
|
-
heading(res.identity.name || "Draft");
|
|
375
|
-
table([
|
|
376
|
-
["Status", res.status],
|
|
377
|
-
["Readiness", `${res.readiness_score}/100`],
|
|
378
|
-
["Tagline", res.identity.tagline],
|
|
379
|
-
["Phone", res.identity.contact?.phone],
|
|
380
|
-
["Email", res.identity.contact?.email],
|
|
381
|
-
["Published as", res.published_practice_id],
|
|
382
|
-
]);
|
|
383
|
-
heading(`Services (${res.services.length})`);
|
|
384
|
-
log(" " + (res.services.join(", ") || c.dim("none found")));
|
|
385
|
-
heading(`Pricing (${res.service_pricing.length})`);
|
|
386
|
-
for (const p of res.service_pricing.slice(0, 12)) {
|
|
387
|
-
log(` ${String(p.label || p.service).padEnd(34)} £${p.amount_from}${p.amount_to ? `–£${p.amount_to}` : ""}`);
|
|
388
|
-
}
|
|
389
|
-
heading(`Clinicians (${res.dentist_candidates.length})`);
|
|
390
|
-
for (const d of res.dentist_candidates.slice(0, 12)) log(` ${d.name}${d.role ? c.dim(` — ${d.role}`) : ""}`);
|
|
391
|
-
if (res.errors) warn(`Notes: ${JSON.stringify(res.errors).slice(0, 300)}`);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
async function cmdPracticePublish(draftId) {
|
|
395
|
-
if (!draftId) { fail("Usage: setu practice publish <draft-id>"); process.exit(1); }
|
|
396
|
-
if (!JSON_OUT && !flags.yes) {
|
|
397
|
-
const proceed = await confirm("Publish this profile as an unclaimed public listing?");
|
|
398
|
-
if (!proceed) { warn("Cancelled."); return; }
|
|
399
|
-
}
|
|
400
|
-
const res = await call("cli-profile-publish", {
|
|
401
|
-
draft_id: draftId,
|
|
402
|
-
create_dentists: !flags["no-dentists"],
|
|
403
|
-
});
|
|
404
|
-
if (JSON_OUT) return out(res);
|
|
405
|
-
ok("Published as an unclaimed listing");
|
|
406
|
-
table([
|
|
407
|
-
["Practice", res.practice_id],
|
|
408
|
-
["Profile", res.profile_url],
|
|
409
|
-
["Claim link", res.claim_url],
|
|
410
|
-
["Clinicians", res.dentists_created],
|
|
411
|
-
]);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function parseCsv(text) {
|
|
415
|
-
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
|
416
|
-
if (!lines.length) return [];
|
|
417
|
-
const split = (line) => {
|
|
418
|
-
const cells = [];
|
|
419
|
-
let cur = "", inQ = false;
|
|
420
|
-
for (const ch of line) {
|
|
421
|
-
if (ch === '"') inQ = !inQ;
|
|
422
|
-
else if (ch === "," && !inQ) { cells.push(cur); cur = ""; }
|
|
423
|
-
else cur += ch;
|
|
424
|
-
}
|
|
425
|
-
cells.push(cur);
|
|
426
|
-
return cells.map((s) => s.trim());
|
|
427
|
-
};
|
|
428
|
-
const header = split(lines[0]).map((h) => h.toLowerCase());
|
|
429
|
-
return lines.slice(1).map((line) => {
|
|
430
|
-
const cells = split(line);
|
|
431
|
-
const row = {};
|
|
432
|
-
header.forEach((h, i) => { if (cells[i]) row[h] = cells[i]; });
|
|
433
|
-
return row;
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
async function cmdPracticeImport(file) {
|
|
438
|
-
if (!file) { fail("Usage: setu practice import <file.csv>"); process.exit(1); }
|
|
439
|
-
let rows;
|
|
440
|
-
try {
|
|
441
|
-
rows = parseCsv(readFileSync(file, "utf8"));
|
|
442
|
-
} catch (e) {
|
|
443
|
-
fail(`Could not read ${file}: ${e.message}`);
|
|
444
|
-
process.exit(1);
|
|
445
|
-
}
|
|
446
|
-
if (!rows.length) { fail("No rows found in that CSV."); process.exit(1); }
|
|
447
|
-
|
|
448
|
-
// Validate before spending any API calls.
|
|
449
|
-
const invalid = [];
|
|
450
|
-
rows = rows.map((row, i) => ({ ...row, __line: i + 2 }));
|
|
451
|
-
const valid = rows.filter((row) => {
|
|
452
|
-
const hasSite = !!(row.website || row.url);
|
|
453
|
-
const hasNameTown = !!(row.name && (row.town || row.city));
|
|
454
|
-
if (!hasSite && !hasNameTown) {
|
|
455
|
-
invalid.push(`line ${row.__line}: needs a website/url, or a name plus town`);
|
|
456
|
-
return false;
|
|
457
|
-
}
|
|
458
|
-
return true;
|
|
459
|
-
});
|
|
460
|
-
if (invalid.length) {
|
|
461
|
-
warn(`Skipping ${invalid.length} unusable row(s):`);
|
|
462
|
-
for (const m of invalid.slice(0, 10)) log(` ${c.dim(m)}`);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Resume: skip anything already completed in a previous results file.
|
|
466
|
-
let done = new Map();
|
|
467
|
-
if (typeof flags.resume === "string") {
|
|
468
|
-
try {
|
|
469
|
-
const prev = JSON.parse(readFileSync(flags.resume, "utf8"));
|
|
470
|
-
for (const r of prev.results ?? []) if (r.ok) done.set(r.label, r);
|
|
471
|
-
} catch (e) { warn(`Could not read resume file: ${e.message}`); }
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const labelOf = (row, i) => row.name || row.website || row.url || `row ${i + 1}`;
|
|
475
|
-
let queue = valid.filter((row, i) => !done.has(labelOf(row, i)));
|
|
476
|
-
if (flags.limit) queue = queue.slice(0, Number(flags.limit));
|
|
477
|
-
|
|
478
|
-
if (flags["dry-run"]) {
|
|
479
|
-
heading(`Dry run — ${queue.length} practice(s) would be built${flags.publish ? " and published" : ""}`);
|
|
480
|
-
for (const row of queue.slice(0, 50)) log(` ${labelOf(row, 0)} ${c.dim(row.website || row.url || row.town || "")}`);
|
|
481
|
-
if (JSON_OUT) out({ dry_run: true, total: queue.length, skipped: invalid.length, already_done: done.size });
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const concurrency = Math.max(1, Math.min(5, Number(flags.concurrency) || 2));
|
|
486
|
-
log(`Importing ${c.bold(queue.length)} practices (${concurrency} at a time)…\n`);
|
|
487
|
-
|
|
488
|
-
const results = [...done.values()];
|
|
489
|
-
let index = 0;
|
|
490
|
-
let completed = 0;
|
|
491
|
-
|
|
492
|
-
async function worker() {
|
|
493
|
-
while (index < queue.length) {
|
|
494
|
-
const i = index++;
|
|
495
|
-
const row = queue[i];
|
|
496
|
-
const label = labelOf(row, i);
|
|
497
|
-
try {
|
|
498
|
-
const created = await call("cli-profile-create", {
|
|
499
|
-
website: row.website || row.url,
|
|
500
|
-
name: row.name,
|
|
501
|
-
town: row.town || row.city,
|
|
502
|
-
postcode: row.postcode,
|
|
503
|
-
contact_email: row.email,
|
|
504
|
-
contact_phone: row.phone,
|
|
505
|
-
});
|
|
506
|
-
let published = null;
|
|
507
|
-
if (flags.publish && created.status !== "failed") {
|
|
508
|
-
published = await call("cli-profile-publish", { draft_id: created.draft_id });
|
|
509
|
-
}
|
|
510
|
-
completed++;
|
|
511
|
-
log(`${c.green("✔")} [${completed}/${queue.length}] ${label} ${c.dim(`(${created.readiness_score}/100)`)}${published ? ` → ${published.claim_url}` : ""}`);
|
|
512
|
-
results.push({
|
|
513
|
-
label, ok: true, draft_id: created.draft_id,
|
|
514
|
-
practice_id: published?.practice_id ?? null,
|
|
515
|
-
claim_url: published?.claim_url ?? null,
|
|
516
|
-
});
|
|
517
|
-
} catch (e) {
|
|
518
|
-
completed++;
|
|
519
|
-
log(`${c.red("✖")} [${completed}/${queue.length}] ${label} — ${e.message}`);
|
|
520
|
-
results.push({ label, ok: false, error: e.message, line: row.__line });
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
await Promise.all(Array.from({ length: concurrency }, worker));
|
|
526
|
-
|
|
527
|
-
const failed = results.filter((r) => !r.ok);
|
|
528
|
-
const payload = {
|
|
529
|
-
total: results.length,
|
|
530
|
-
built: results.length - failed.length,
|
|
531
|
-
failed: failed.length,
|
|
532
|
-
skipped_invalid: invalid.length,
|
|
533
|
-
results,
|
|
534
|
-
};
|
|
535
|
-
|
|
536
|
-
if (typeof flags.out === "string") {
|
|
537
|
-
try {
|
|
538
|
-
writeFileSync(flags.out, JSON.stringify(payload, null, 2));
|
|
539
|
-
ok(`Results written to ${flags.out}${failed.length ? ` — rerun with --resume ${flags.out}` : ""}`);
|
|
540
|
-
} catch (e) { warn(`Could not write results file: ${e.message}`); }
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
if (JSON_OUT) { out(payload); }
|
|
544
|
-
else {
|
|
545
|
-
heading(`Done — ${payload.built} built, ${failed.length} failed`);
|
|
546
|
-
for (const f of failed) log(` ${c.red("✖")} ${f.label}: ${f.error}`);
|
|
547
|
-
}
|
|
548
|
-
if (failed.length) process.exitCode = 1;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
async function cmdClaimLink(id) {
|
|
552
|
-
if (!id) { fail("Usage: setu practice claim-link <practice-id>"); process.exit(1); }
|
|
553
|
-
const res = await call("cli-claim-link", {
|
|
554
|
-
id,
|
|
555
|
-
profile_type: type === "dentist" || flags.dentist ? "dentist" : "practice",
|
|
556
|
-
rotate: !!flags.rotate,
|
|
557
|
-
});
|
|
558
|
-
if (JSON_OUT) return out(res);
|
|
559
|
-
ok(res.name);
|
|
560
|
-
log(` ${res.claim_url}`);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
async function cmdDentistCreate() {
|
|
564
|
-
if (typeof flags.name !== "string") { fail("Pass --name \"Dr Jane Roe\"."); process.exit(1); }
|
|
565
|
-
const s = spinner("Building the clinician profile");
|
|
566
|
-
try {
|
|
567
|
-
const res = await call("cli-dentist-create", {
|
|
568
|
-
name: flags.name,
|
|
569
|
-
practice_id: typeof flags.practice === "string" ? flags.practice : undefined,
|
|
570
|
-
profile_url: typeof flags["profile-url"] === "string" ? flags["profile-url"] : undefined,
|
|
571
|
-
town: typeof flags.town === "string" ? flags.town : undefined,
|
|
572
|
-
postcode: typeof flags.postcode === "string" ? flags.postcode : undefined,
|
|
573
|
-
email: typeof flags.email === "string" ? flags.email : undefined,
|
|
574
|
-
phone: typeof flags.phone === "string" ? flags.phone : undefined,
|
|
575
|
-
website_url: typeof flags.website === "string" ? flags.website : undefined,
|
|
576
|
-
booking_url: typeof flags.booking === "string" ? flags.booking : undefined,
|
|
577
|
-
bio: typeof flags.bio === "string" ? flags.bio : undefined,
|
|
578
|
-
photo_url: typeof flags.photo === "string" ? flags.photo : undefined,
|
|
579
|
-
gallery: splitList(flags.gallery),
|
|
580
|
-
});
|
|
581
|
-
s.stop();
|
|
582
|
-
if (JSON_OUT) return out(res);
|
|
583
|
-
ok("Clinician seeded as unclaimed");
|
|
584
|
-
table([
|
|
585
|
-
["Dentist", res.dentist_id],
|
|
586
|
-
["AI enriched", res.enriched ? "yes" : "no"],
|
|
587
|
-
["Photo", res.photo_url ?? "—"],
|
|
588
|
-
["Claim link", res.claim_url],
|
|
589
|
-
]);
|
|
590
|
-
} catch (e) { s.stop(); throw e; }
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function printProfileList(res) {
|
|
594
|
-
heading(`Seeded ${res.profile_type === "dentist" ? "clinicians" : "practices"} (${res.items.length})`);
|
|
595
|
-
for (const p of res.items) {
|
|
596
|
-
const state = p.claim_status === "claimed" ? c.green("claimed") : p.is_live ? c.green("live") : c.yellow(p.claim_status);
|
|
597
|
-
log(` ${state.padEnd(20)} ${String(p.name).slice(0, 38).padEnd(40)} ${c.dim(p.id)}`);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
async function cmdProfileList(type) {
|
|
602
|
-
const res = await call("cli-profile-status", {
|
|
603
|
-
list: true,
|
|
604
|
-
profile_type: type,
|
|
605
|
-
limit: flags.limit,
|
|
606
|
-
search: typeof flags.search === "string" ? flags.search : undefined,
|
|
607
|
-
practice_id: type === "dentist" && typeof flags.practice === "string" ? flags.practice : undefined,
|
|
608
|
-
});
|
|
609
|
-
if (JSON_OUT) return out(res);
|
|
610
|
-
printProfileList(res);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
async function cmdStatus(id, type = flags.dentist ? "dentist" : "practice") {
|
|
614
|
-
const res = await call(
|
|
615
|
-
"cli-profile-status",
|
|
616
|
-
id
|
|
617
|
-
? { id, profile_type: type, full: !!flags.full }
|
|
618
|
-
: { list: true, profile_type: type, limit: flags.limit, search: typeof flags.search === "string" ? flags.search : undefined },
|
|
619
|
-
);
|
|
620
|
-
if (JSON_OUT) return out(res);
|
|
621
|
-
if (res.items) return printProfileList(res);
|
|
622
|
-
|
|
623
|
-
const p = res.profile ?? res.practice;
|
|
624
|
-
heading(p.name);
|
|
625
|
-
table([
|
|
626
|
-
["Type", res.profile_type],
|
|
627
|
-
["Status", p.claim_status],
|
|
628
|
-
["Live", p.is_live ? "yes" : "no"],
|
|
629
|
-
["Source", `${p.source} (${p.created_by_operator ?? "—"})`],
|
|
630
|
-
["Claim link", p.claim_url],
|
|
631
|
-
]);
|
|
632
|
-
|
|
633
|
-
if (flags.full) {
|
|
634
|
-
const gallery = res.gallery ?? [];
|
|
635
|
-
heading(`Photos (${gallery.length})`);
|
|
636
|
-
gallery.forEach((url, i) => log(` ${String(i).padEnd(3)} ${c.dim(url)}`));
|
|
637
|
-
log(` ${c.dim(res.profile_type === "dentist" ? "main photo" : "logo")}: ${res.logo ?? c.dim("none")}`);
|
|
638
|
-
|
|
639
|
-
const pricing = res.pricing ?? [];
|
|
640
|
-
heading(`Prices (${pricing.length})`);
|
|
641
|
-
for (const row of pricing) {
|
|
642
|
-
const amount = row.amount_to ? `${row.amount}–${row.amount_to}` : row.amount ?? "—";
|
|
643
|
-
log(` ${String(row.label).slice(0, 38).padEnd(40)} ${amount}`);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
if (res.dentists) {
|
|
647
|
-
heading(`Clinicians (${res.dentists.length})`);
|
|
648
|
-
for (const d of res.dentists) log(` ${String(d.name).slice(0, 38).padEnd(40)} ${c.dim(d.id)}`);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
heading(`Claims (${res.claims.length})`);
|
|
653
|
-
for (const cl of res.claims) log(` ${cl.status.padEnd(10)} ${cl.claimant_email} ${c.dim(cl.method)}`);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
function printClaimRow(cl) {
|
|
657
|
-
const who = String(cl.claimant_email ?? "—").slice(0, 30).padEnd(32);
|
|
658
|
-
const name = String(cl.profile?.name ?? cl.profile_type ?? "—").slice(0, 26).padEnd(28);
|
|
659
|
-
const flag = cl.awaiting_signin ? c.yellow(" (not signed in)") : "";
|
|
660
|
-
log(` ${String(cl.status).padEnd(9)} ${who} ${name} ${c.dim(cl.method ?? "")}${flag}`);
|
|
661
|
-
log(` ${c.dim(cl.id)}`);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
async function cmdClaims(sub, arg) {
|
|
665
|
-
if (sub === "show") {
|
|
666
|
-
if (!arg) throw new Error("Usage: setu claims show <claim-id>");
|
|
667
|
-
const res = await call("cli-claims", { action: "detail", claim_id: arg });
|
|
668
|
-
if (JSON_OUT) return out(res);
|
|
669
|
-
heading(`Claim ${res.claim.id}`);
|
|
670
|
-
table([
|
|
671
|
-
["Status", res.claim.status],
|
|
672
|
-
["Claimant", res.claim.claimant_email],
|
|
673
|
-
["Method", res.claim.method],
|
|
674
|
-
["Attempts", res.claim.attempts],
|
|
675
|
-
["Signed in", res.claim.claimant_user_id ? "yes" : "no"],
|
|
676
|
-
["Profile", res.profile ? `${res.profile.name} (${res.claim.profile_type})` : "—"],
|
|
677
|
-
["Readiness", res.profile ? `${res.profile.score}%` : "—"],
|
|
678
|
-
["Missing", res.profile?.missing?.join(", ")],
|
|
679
|
-
["Claim link", res.profile?.claim_url],
|
|
680
|
-
]);
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
if (sub === "approve" || sub === "decline" || sub === "reject") {
|
|
685
|
-
if (!arg) throw new Error(`Usage: setu claims ${sub} <claim-id>`);
|
|
686
|
-
if (!flags.yes && !JSON_OUT) {
|
|
687
|
-
const go = await confirm(`${sub === "approve" ? "Approve" : "Decline"} claim ${arg}?`);
|
|
688
|
-
if (!go) return warn("Cancelled.");
|
|
689
|
-
}
|
|
690
|
-
const res = await call("cli-claims", {
|
|
691
|
-
action: sub === "approve" ? "approve" : "decline",
|
|
692
|
-
claim_id: arg,
|
|
693
|
-
reason: typeof flags.reason === "string" ? flags.reason : undefined,
|
|
694
|
-
});
|
|
695
|
-
if (JSON_OUT) return out(res);
|
|
696
|
-
return ok(`Claim ${res.status} — ${res.claim_id}`);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
if (sub && sub !== "list") throw new Error(`Unknown command: claims ${sub}`);
|
|
700
|
-
|
|
701
|
-
const status = typeof flags.status === "string" ? flags.status : "pending";
|
|
702
|
-
const profileType = flags.dentist ? "dentist" : undefined;
|
|
703
|
-
if (sub === "list" || (typeof flags.status === "string" && flags.status !== "pending")) {
|
|
704
|
-
const res = await call("cli-claims", { action: "list", status, profile_type: profileType, limit: flags.limit });
|
|
705
|
-
if (JSON_OUT) return out(res);
|
|
706
|
-
heading(`Claims — ${status} (${res.claims.length})`);
|
|
707
|
-
res.claims.forEach(printClaimRow);
|
|
708
|
-
if (!res.claims.length) log(c.dim(" none"));
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
const res = await call("cli-claims", {
|
|
713
|
-
action: "dashboard",
|
|
714
|
-
profile_type: profileType,
|
|
715
|
-
ready_only: !!flags.ready,
|
|
716
|
-
limit: flags.limit,
|
|
717
|
-
});
|
|
718
|
-
if (JSON_OUT) return out(res);
|
|
719
|
-
|
|
720
|
-
heading("Claims");
|
|
721
|
-
table([
|
|
722
|
-
["Pending", res.counts.pending],
|
|
723
|
-
["Approved", res.counts.approved],
|
|
724
|
-
["Rejected", res.counts.rejected],
|
|
725
|
-
]);
|
|
726
|
-
|
|
727
|
-
heading(`Pending review (${res.pending_claims.length})`);
|
|
728
|
-
res.pending_claims.forEach(printClaimRow);
|
|
729
|
-
if (!res.pending_claims.length) log(c.dim(" nothing waiting"));
|
|
730
|
-
|
|
731
|
-
heading(`Unclaimed profiles — ${res.unclaimed.ready}/${res.unclaimed.total} ready, ${res.unclaimed.not_live} ready but offline`);
|
|
732
|
-
for (const u of res.unclaimed.items) {
|
|
733
|
-
const badge = u.ready ? c.green("ready") : c.yellow("gaps ");
|
|
734
|
-
log(` ${badge} ${String(u.readiness_score + "%").padStart(4)} ${String(u.name).slice(0, 30).padEnd(32)} ${c.dim(u.profile_type)}${u.is_live ? "" : c.dim(" offline")}`);
|
|
735
|
-
if (u.missing.length) log(` ${c.dim("missing: " + u.missing.join(", "))}`);
|
|
736
|
-
}
|
|
737
|
-
if (!res.unclaimed.items.length) log(c.dim(" none"));
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
async function main() {
|
|
741
|
-
if (typeof flags.operator === "string") {
|
|
742
|
-
const cfg = loadConfig();
|
|
743
|
-
saveConfig({ ...cfg, operator: flags.operator });
|
|
744
|
-
}
|
|
745
|
-
if (flags.version) { log(`setu ${VERSION}`); return; }
|
|
746
|
-
const [group, sub, arg] = positional;
|
|
747
|
-
if (!group || flags.help || group === "help") { usage(); return; }
|
|
748
|
-
switch (group) {
|
|
749
|
-
case "login": return cmdLogin();
|
|
750
|
-
case "logout": return cmdLogout();
|
|
751
|
-
case "whoami": return cmdWhoami();
|
|
752
|
-
case "status": return cmdStatus(sub);
|
|
753
|
-
case "claims": return cmdClaims(sub, arg);
|
|
754
|
-
case "practice":
|
|
755
|
-
switch (sub) {
|
|
756
|
-
case "create": return cmdPracticeCreate();
|
|
757
|
-
case "review": return cmdPracticeReview(arg);
|
|
758
|
-
case "publish": return cmdPracticePublish(arg);
|
|
759
|
-
case "import": return cmdPracticeImport(arg);
|
|
760
|
-
case "claim-link": return cmdClaimLink(arg);
|
|
761
|
-
case "delete": return cmdPracticeDelete(arg);
|
|
762
|
-
case "update": return cmdProfileUpdate("practice", arg);
|
|
763
|
-
case "images": return cmdProfileImages("practice", arg);
|
|
764
|
-
case "pricing": return cmdProfilePricing("practice", arg);
|
|
765
|
-
case "list": return cmdProfileList("practice");
|
|
766
|
-
default:
|
|
767
|
-
fail(`Unknown command: practice ${sub ?? ""}`.trim());
|
|
768
|
-
usage();
|
|
769
|
-
process.exitCode = 1;
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
case "dentist":
|
|
773
|
-
if (sub === "create") return cmdDentistCreate();
|
|
774
|
-
if (sub === "list") return cmdProfileList("dentist");
|
|
775
|
-
if (sub === "status") return cmdStatus(arg, "dentist");
|
|
776
|
-
if (sub === "update") return cmdProfileUpdate("dentist", arg);
|
|
777
|
-
if (sub === "images") return cmdProfileImages("dentist", arg);
|
|
778
|
-
if (sub === "pricing") return cmdProfilePricing("dentist", arg);
|
|
779
|
-
if (sub === "delete") return cmdPracticeDelete(arg, "dentist");
|
|
780
|
-
fail(`Unknown command: dentist ${sub ?? ""}`.trim());
|
|
781
|
-
usage();
|
|
782
|
-
process.exitCode = 1;
|
|
783
|
-
return;
|
|
784
|
-
default:
|
|
785
|
-
fail(`Unknown command: ${group}`);
|
|
786
|
-
usage();
|
|
787
|
-
process.exitCode = 1;
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
main().catch((e) => {
|
|
793
|
-
fail(e.message);
|
|
794
|
-
if (flags.verbose && e.stack) log(c.dim(e.stack));
|
|
795
|
-
if (e.data && !JSON_OUT && e.data.practice_id) log(c.dim(` existing: ${e.data.practice_id}`));
|
|
796
|
-
process.exit(1);
|
|
797
|
-
});
|