frontpage-host 0.1.12 → 0.1.14
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/AGENTS.md +2 -1
- package/dist/commands/help.js +9 -1
- package/dist/commands/modules.js +74 -22
- package/dist/completions/generate.js +21 -3
- package/dist/http/client.d.ts +8 -2
- package/dist/http/client.js +16 -6
- package/dist/modules/commands.d.ts +26 -0
- package/dist/modules/commands.js +299 -0
- package/dist/modules/google_reviews.d.ts +1 -0
- package/dist/modules/google_reviews.js +228 -0
- package/dist/modules/typed_config.d.ts +4 -0
- package/dist/modules/typed_config.js +146 -0
- package/dist/registry.js +26 -4
- package/llms.txt +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -109,7 +109,8 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
|
|
|
109
109
|
## Modules & articles
|
|
110
110
|
|
|
111
111
|
- Forms: `data-fp-form` markup in pages + `frontpage modules` for enable/config. No submit JS / honeypot / `action`.
|
|
112
|
-
- Wizard modules (payments/shop/ordering/
|
|
112
|
+
- Wizard / connect-once modules (payments/shop/ordering/google_reviews): after building gated markup, `frontpage modules request-setup <key> --reason "…"` once — owner finishes in the **editor status bar** (CLI cannot show it). Auto-active modules (forms/callbar/…) need markup, not request-setup. Typed settings: `frontpage modules callbar|forms|booking|signups get|set` (forms set needs `--instance`; do not target `default`). `frontpage modules <key>` prints that key's CLI usage (exit 2). Index: https://frontpage.host/developers/modules
|
|
113
|
+
- Google reviews: platform nodes only (`data-fp-google-reviews` / `data-fp-google-ask` + required `data-fp-requires="google_reviews"`). `data-layout` is `feed` (default), `badge`, or `link`. Never invent stars, quotes, a 1–10, or a Maps URL. Do not pick a Place ID via Autocomplete and do not write `placeId` through `modules config`. Owner-supplied Place ID / Maps URL: `frontpage modules google_reviews connect --place-id …` or `--maps-url …` (server resolves + seeds). Curation: `list-reviews`, `hide --id`, `show --id`, `set-stars`, `set-sort`, `set-weight`, `reset-weights`, `refresh`, `disconnect`. Usage: `frontpage modules google_reviews`. Docs: https://frontpage.host/developers/modules/google-reviews. Ask-widget scores: `frontpage inbox google_reviews`.
|
|
113
114
|
- Articles: `frontpage articles …` for lifecycle; bodies via sync under `content/` or `articles content get|set`. Section landing is an owned `src/pages/<section>/index.astro`.
|
|
114
115
|
- Migrate a folder into Articles: prefer `frontpage prompt "migrate … into articles"` — do not invent a mass delete+write migration.
|
|
115
116
|
- Extensions: `frontpage extensions`; if LOCKED on plan, do not fake charts/etc.
|
package/dist/commands/help.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { COMMANDS, TOPIC_NAMES, TOPICS, findCommand } from "../registry.js";
|
|
2
|
+
import { MODULE_COMMANDS, formatModulesIndex } from "../modules/commands.js";
|
|
2
3
|
import { isJson, emitOk } from "../runtime.js";
|
|
3
4
|
function printCommandHelp(cmd) {
|
|
4
5
|
console.log(`${cmd.name} — ${cmd.summary}`);
|
|
@@ -41,6 +42,7 @@ export function runHelp(args) {
|
|
|
41
42
|
})),
|
|
42
43
|
topics: TOPIC_NAMES,
|
|
43
44
|
topicBodies: TOPICS,
|
|
45
|
+
moduleCommands: MODULE_COMMANDS,
|
|
44
46
|
});
|
|
45
47
|
return 0;
|
|
46
48
|
}
|
|
@@ -50,7 +52,10 @@ export function runHelp(args) {
|
|
|
50
52
|
}
|
|
51
53
|
const cmd = findCommand(topic);
|
|
52
54
|
if (cmd) {
|
|
53
|
-
emitOk("help", {
|
|
55
|
+
emitOk("help", {
|
|
56
|
+
command: cmd,
|
|
57
|
+
...(cmd.name === "modules" ? { moduleCommands: MODULE_COMMANDS } : {}),
|
|
58
|
+
});
|
|
54
59
|
return 0;
|
|
55
60
|
}
|
|
56
61
|
console.log(JSON.stringify({
|
|
@@ -81,6 +86,9 @@ export function runHelp(args) {
|
|
|
81
86
|
const cmd = findCommand(topic);
|
|
82
87
|
if (cmd) {
|
|
83
88
|
printCommandHelp(cmd);
|
|
89
|
+
if (cmd.name === "modules") {
|
|
90
|
+
console.log(`\n${formatModulesIndex()}`);
|
|
91
|
+
}
|
|
84
92
|
return 0;
|
|
85
93
|
}
|
|
86
94
|
console.error(`Unknown help topic: ${topic}`);
|
package/dist/commands/modules.js
CHANGED
|
@@ -1,9 +1,59 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
|
-
import { EXIT_OK,
|
|
2
|
+
import { EXIT_OK, exitCodeForError, v1Get, v1Post } from "../http/client.js";
|
|
3
3
|
import { takeFlag } from "../argv.js";
|
|
4
|
+
import { emitUsage } from "../runtime.js";
|
|
5
|
+
import { formatModuleUsage, formatModulesIndex, isCliModuleAction, isDocumentedModuleKey, isGenericModuleVerb, } from "../modules/commands.js";
|
|
6
|
+
import { runGoogleReviews } from "../modules/google_reviews.js";
|
|
7
|
+
import { isTypedConfigCliKey, runTypedConfig } from "../modules/typed_config.js";
|
|
8
|
+
/** Drop --site so we can print local usage without resolving a checkout. */
|
|
9
|
+
function positionalArgs(args) {
|
|
10
|
+
const rest = [];
|
|
11
|
+
for (let i = 0; i < args.length; i++) {
|
|
12
|
+
const a = args[i];
|
|
13
|
+
if (a === "--site" || a === "-s") {
|
|
14
|
+
i += 1;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (a.startsWith("--site="))
|
|
18
|
+
continue;
|
|
19
|
+
rest.push(a);
|
|
20
|
+
}
|
|
21
|
+
return rest;
|
|
22
|
+
}
|
|
23
|
+
function rewriteKeyFirst(rest) {
|
|
24
|
+
const key = rest[0];
|
|
25
|
+
const action = rest[1];
|
|
26
|
+
if (key && action && isDocumentedModuleKey(key) && isGenericModuleVerb(action)) {
|
|
27
|
+
return [action, key, ...rest.slice(2)];
|
|
28
|
+
}
|
|
29
|
+
return rest;
|
|
30
|
+
}
|
|
4
31
|
export async function runModules(args) {
|
|
5
32
|
try {
|
|
6
|
-
const
|
|
33
|
+
const peeked = positionalArgs(args);
|
|
34
|
+
const peekedKey = peeked[0];
|
|
35
|
+
const peekedAction = peeked[1];
|
|
36
|
+
if (peekedKey && isDocumentedModuleKey(peekedKey)) {
|
|
37
|
+
if (!peekedAction) {
|
|
38
|
+
return emitUsage("modules", formatModuleUsage(peekedKey));
|
|
39
|
+
}
|
|
40
|
+
if (isCliModuleAction(peekedKey, peekedAction)) {
|
|
41
|
+
const { slug } = await resolveSlug(args);
|
|
42
|
+
const extraFlags = positionalArgs(args).slice(2);
|
|
43
|
+
if (peekedKey === "google_reviews") {
|
|
44
|
+
return await runGoogleReviews(slug, peekedAction, extraFlags);
|
|
45
|
+
}
|
|
46
|
+
if (isTypedConfigCliKey(peekedKey)) {
|
|
47
|
+
return await runTypedConfig(peekedKey, slug, peekedAction, extraFlags);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!isGenericModuleVerb(peekedAction)) {
|
|
51
|
+
return emitUsage("modules", `${formatModuleUsage(peekedKey)}\n\nUnknown action "${peekedAction}". Not in this CLI yet — use Modules or the editor.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const resolved = await resolveSlug(args);
|
|
55
|
+
const slug = resolved.slug;
|
|
56
|
+
const rest = rewriteKeyFirst(resolved.rest);
|
|
7
57
|
const sub = rest[0];
|
|
8
58
|
if (!sub || sub === "list" || sub === "ls") {
|
|
9
59
|
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/modules`);
|
|
@@ -16,12 +66,16 @@ export async function runModules(args) {
|
|
|
16
66
|
const key = rest[1];
|
|
17
67
|
if (!key) {
|
|
18
68
|
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/modules`);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
69
|
+
const known = data.modules
|
|
70
|
+
.map((m) => ` ${m.key.padEnd(16)} ${m.name}`)
|
|
71
|
+
.join("\n");
|
|
72
|
+
return emitUsage("modules", [
|
|
73
|
+
"Usage: frontpage modules docs <key> [--instance <id>] [--site <slug>]",
|
|
74
|
+
"Known modules (run: frontpage modules docs <key>):",
|
|
75
|
+
known,
|
|
76
|
+
"",
|
|
77
|
+
formatModulesIndex(),
|
|
78
|
+
].join("\n"));
|
|
25
79
|
}
|
|
26
80
|
const work = rest.slice(2);
|
|
27
81
|
const instanceId = takeFlag(work, "--instance");
|
|
@@ -37,16 +91,14 @@ export async function runModules(args) {
|
|
|
37
91
|
if (sub === "request-setup" || sub === "request_setup") {
|
|
38
92
|
const key = rest[1];
|
|
39
93
|
if (!key) {
|
|
40
|
-
|
|
41
|
-
return EXIT_USAGE;
|
|
94
|
+
return emitUsage("modules", 'Usage: frontpage modules request-setup <key> --reason "…" [--prefill \'{…}\'] [--instance <id>] [--site <slug>]');
|
|
42
95
|
}
|
|
43
96
|
const work = rest.slice(2);
|
|
44
97
|
const instanceId = takeFlag(work, "--instance");
|
|
45
98
|
const reason = takeFlag(work, "--reason");
|
|
46
99
|
const prefillRaw = takeFlag(work, "--prefill");
|
|
47
100
|
if (!reason) {
|
|
48
|
-
|
|
49
|
-
return EXIT_USAGE;
|
|
101
|
+
return emitUsage("modules", 'Usage: frontpage modules request-setup <key> --reason "…" [--prefill \'{…}\']');
|
|
50
102
|
}
|
|
51
103
|
let prefill;
|
|
52
104
|
if (prefillRaw) {
|
|
@@ -54,8 +106,7 @@ export async function runModules(args) {
|
|
|
54
106
|
prefill = JSON.parse(prefillRaw);
|
|
55
107
|
}
|
|
56
108
|
catch {
|
|
57
|
-
|
|
58
|
-
return EXIT_USAGE;
|
|
109
|
+
return emitUsage("modules", "--prefill must be valid JSON");
|
|
59
110
|
}
|
|
60
111
|
}
|
|
61
112
|
const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/modules/${encodeURIComponent(key)}`, { action: "request-setup", reason, prefill, instanceId });
|
|
@@ -65,8 +116,7 @@ export async function runModules(args) {
|
|
|
65
116
|
if (sub === "enable" || sub === "disable" || sub === "archive" || sub === "config") {
|
|
66
117
|
const key = rest[1];
|
|
67
118
|
if (!key) {
|
|
68
|
-
|
|
69
|
-
return EXIT_USAGE;
|
|
119
|
+
return emitUsage("modules", `Usage: frontpage modules ${sub} <key> [--instance <id>] [--config '{…}']`);
|
|
70
120
|
}
|
|
71
121
|
const work = rest.slice(2);
|
|
72
122
|
const instanceId = takeFlag(work, "--instance");
|
|
@@ -74,23 +124,25 @@ export async function runModules(args) {
|
|
|
74
124
|
if (sub === "config") {
|
|
75
125
|
const raw = takeFlag(work, "--config") ?? work[0];
|
|
76
126
|
if (!raw) {
|
|
77
|
-
|
|
78
|
-
return EXIT_USAGE;
|
|
127
|
+
return emitUsage("modules", "Usage: frontpage modules config <key> --config '{…json…}'");
|
|
79
128
|
}
|
|
80
129
|
try {
|
|
81
130
|
config = JSON.parse(raw);
|
|
82
131
|
}
|
|
83
132
|
catch {
|
|
84
|
-
|
|
85
|
-
return EXIT_USAGE;
|
|
133
|
+
return emitUsage("modules", "--config must be valid JSON");
|
|
86
134
|
}
|
|
87
135
|
}
|
|
88
136
|
const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/modules/${encodeURIComponent(key)}`, { action: sub, config, instanceId });
|
|
89
137
|
console.log(JSON.stringify(data, null, 2));
|
|
90
138
|
return EXIT_OK;
|
|
91
139
|
}
|
|
92
|
-
|
|
93
|
-
|
|
140
|
+
return emitUsage("modules", [
|
|
141
|
+
"Usage: frontpage modules [list|docs|request-setup|enable|disable|archive|config] …",
|
|
142
|
+
" frontpage modules <key> # that key's CLI usage",
|
|
143
|
+
"",
|
|
144
|
+
formatModulesIndex(),
|
|
145
|
+
].join("\n"));
|
|
94
146
|
}
|
|
95
147
|
catch (err) {
|
|
96
148
|
console.error(err instanceof Error ? err.message : String(err));
|
|
@@ -24,7 +24,15 @@ _frontpage_completions() {
|
|
|
24
24
|
seo|settings) COMPREPLY=( $(compgen -W "get set" -- "\${cur}") ) ;;
|
|
25
25
|
articles) COMPREPLY=( $(compgen -W "list content create-section create-article set-draft" -- "\${cur}") ) ;;
|
|
26
26
|
analytics) COMPREPLY=( $(compgen -W "dashboard summary query funnel timeseries compare sql" -- "\${cur}") ) ;;
|
|
27
|
-
modules)
|
|
27
|
+
modules)
|
|
28
|
+
if [[ \${COMP_CWORD} -ge 3 && \${COMP_WORDS[2]} == google_reviews ]]; then
|
|
29
|
+
COMPREPLY=( $(compgen -W "docs request-setup enable disable archive config connect resolve status list-reviews hide show set-stars set-sort set-weight reset-weights write-review-url refresh disconnect" -- "\${cur}") )
|
|
30
|
+
elif [[ \${COMP_CWORD} -ge 3 && (\${COMP_WORDS[2]} == callbar || \${COMP_WORDS[2]} == forms || \${COMP_WORDS[2]} == booking || \${COMP_WORDS[2]} == signups) ]]; then
|
|
31
|
+
COMPREPLY=( $(compgen -W "docs request-setup enable disable archive config get set" -- "\${cur}") )
|
|
32
|
+
else
|
|
33
|
+
COMPREPLY=( $(compgen -W "list docs request-setup enable disable archive config callbar forms booking signups payments shop ordering articles google_reviews" -- "\${cur}") )
|
|
34
|
+
fi
|
|
35
|
+
;;
|
|
28
36
|
extensions) COMPREPLY=( $(compgen -W "list docs enable disable config" -- "\${cur}") ) ;;
|
|
29
37
|
prompt) COMPREPLY=( $(compgen -W "--path --attach --asset-url --site --json --help" -- "\${cur}") ) ;;
|
|
30
38
|
*) COMPREPLY=( $(compgen -W "--site --help --json --quiet --yes" -- "\${cur}") ) ;;
|
|
@@ -53,7 +61,15 @@ _frontpage() {
|
|
|
53
61
|
tokens) _values 'subcommand' set ;;
|
|
54
62
|
seo|settings) _values 'subcommand' get set ;;
|
|
55
63
|
analytics) _values 'subcommand' dashboard summary query funnel timeseries compare sql ;;
|
|
56
|
-
modules)
|
|
64
|
+
modules)
|
|
65
|
+
if [[ $words[2] == google_reviews ]]; then
|
|
66
|
+
_values 'subcommand' docs request-setup enable disable archive config connect resolve status list-reviews hide show set-stars set-sort set-weight reset-weights write-review-url refresh disconnect
|
|
67
|
+
elif [[ $words[2] == callbar || $words[2] == forms || $words[2] == booking || $words[2] == signups ]]; then
|
|
68
|
+
_values 'subcommand' docs request-setup enable disable archive config get set
|
|
69
|
+
else
|
|
70
|
+
_values 'subcommand' list docs request-setup enable disable archive config callbar forms booking signups payments shop ordering articles google_reviews
|
|
71
|
+
fi
|
|
72
|
+
;;
|
|
57
73
|
extensions) _values 'subcommand' list docs enable disable config ;;
|
|
58
74
|
esac
|
|
59
75
|
;;
|
|
@@ -75,7 +91,9 @@ export function generateFishCompletion() {
|
|
|
75
91
|
`complete -c frontpage -n "__fish_seen_subcommand_from tokens" -a "set"`,
|
|
76
92
|
`complete -c frontpage -n "__fish_seen_subcommand_from seo settings" -a "get set"`,
|
|
77
93
|
`complete -c frontpage -n "__fish_seen_subcommand_from analytics" -a "dashboard summary query funnel timeseries compare sql"`,
|
|
78
|
-
`complete -c frontpage -n "__fish_seen_subcommand_from modules" -a "list docs request-setup enable disable archive config"`,
|
|
94
|
+
`complete -c frontpage -n "__fish_seen_subcommand_from modules" -a "list docs request-setup enable disable archive config callbar forms booking signups payments shop ordering articles google_reviews"`,
|
|
95
|
+
`complete -c frontpage -n "__fish_seen_subcommand_from modules; and __fish_seen_subcommand_from google_reviews" -a "connect resolve status list-reviews hide show set-stars set-sort set-weight reset-weights write-review-url refresh disconnect"`,
|
|
96
|
+
`complete -c frontpage -n "__fish_seen_subcommand_from modules; and __fish_seen_subcommand_from callbar forms booking signups" -a "get set"`,
|
|
79
97
|
`complete -c frontpage -n "__fish_seen_subcommand_from extensions" -a "list docs enable disable config"`,
|
|
80
98
|
`complete -c frontpage -l site -d "Site subdomain slug"`,
|
|
81
99
|
];
|
package/dist/http/client.d.ts
CHANGED
|
@@ -23,8 +23,14 @@ export declare function httpErrorFromV1(status: number, parsed: V1Envelope, fall
|
|
|
23
23
|
export declare function apiBase(): string;
|
|
24
24
|
/** Bearer header for raw fetch (e.g. Blob client-token exchange). */
|
|
25
25
|
export declare function getBearerAuthHeader(): Promise<Record<string, string>>;
|
|
26
|
-
|
|
27
|
-
export declare
|
|
26
|
+
/** Connect / refresh can seed up to ~300s. Other calls stay on the default fetch timeout. */
|
|
27
|
+
export declare const LONG_MODULE_TIMEOUT_MS = 310000;
|
|
28
|
+
export declare function v1Get<T>(path: string, opts?: {
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
}): Promise<T>;
|
|
31
|
+
export declare function v1Post<T>(path: string, body?: unknown, opts?: {
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
}): Promise<T>;
|
|
28
34
|
export declare function v1Put<T>(path: string, body?: unknown): Promise<T>;
|
|
29
35
|
export declare function v1Delete<T>(path: string): Promise<T>;
|
|
30
36
|
export declare function v1Patch<T>(path: string, body?: unknown): Promise<T>;
|
package/dist/http/client.js
CHANGED
|
@@ -69,7 +69,9 @@ export async function getBearerAuthHeader() {
|
|
|
69
69
|
}
|
|
70
70
|
return { Authorization: `Bearer ${key}` };
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
/** Connect / refresh can seed up to ~300s. Other calls stay on the default fetch timeout. */
|
|
73
|
+
export const LONG_MODULE_TIMEOUT_MS = 310_000;
|
|
74
|
+
async function v1Request(method, path, body, opts) {
|
|
73
75
|
const key = await getApiKey();
|
|
74
76
|
if (!key) {
|
|
75
77
|
throw new CliHttpError("Not logged in. Run: frontpage login", EXIT_AUTH, 401);
|
|
@@ -84,11 +86,19 @@ async function v1Request(method, path, body) {
|
|
|
84
86
|
headers["Content-Type"] = "application/json";
|
|
85
87
|
payload = JSON.stringify(body);
|
|
86
88
|
}
|
|
89
|
+
const init = { method, headers, body: payload };
|
|
90
|
+
if (opts?.timeoutMs && opts.timeoutMs > 0) {
|
|
91
|
+
init.signal = AbortSignal.timeout(opts.timeoutMs);
|
|
92
|
+
}
|
|
87
93
|
let res;
|
|
88
94
|
try {
|
|
89
|
-
res = await fetch(url,
|
|
95
|
+
res = await fetch(url, init);
|
|
90
96
|
}
|
|
91
97
|
catch (err) {
|
|
98
|
+
const name = err instanceof Error ? err.name : "";
|
|
99
|
+
if (name === "TimeoutError" || name === "AbortError") {
|
|
100
|
+
throw new CliHttpError("Request timed out.", EXIT_UNEXPECTED, 0);
|
|
101
|
+
}
|
|
92
102
|
throw new CliHttpError(`Network error: ${err instanceof Error ? err.message : String(err)}`, EXIT_UNEXPECTED, 0);
|
|
93
103
|
}
|
|
94
104
|
const text = await res.text();
|
|
@@ -103,11 +113,11 @@ async function v1Request(method, path, body) {
|
|
|
103
113
|
return parsed;
|
|
104
114
|
throw httpErrorFromV1(res.status, parsed);
|
|
105
115
|
}
|
|
106
|
-
export async function v1Get(path) {
|
|
107
|
-
return v1Request("GET", path);
|
|
116
|
+
export async function v1Get(path, opts) {
|
|
117
|
+
return v1Request("GET", path, undefined, opts);
|
|
108
118
|
}
|
|
109
|
-
export async function v1Post(path, body) {
|
|
110
|
-
return v1Request("POST", path, body ?? {});
|
|
119
|
+
export async function v1Post(path, body, opts) {
|
|
120
|
+
return v1Request("POST", path, body ?? {}, opts);
|
|
111
121
|
}
|
|
112
122
|
export async function v1Put(path, body) {
|
|
113
123
|
return v1Request("PUT", path, body ?? {});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §CLI-MOD-FW — per-key CLI registry (usage printer + help --json).
|
|
3
|
+
* Instagram is not documented here. Google reviews extras ship in Phase 1.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DOCUMENTED_MODULE_KEYS: readonly ["callbar", "forms", "booking", "signups", "payments", "shop", "ordering", "articles", "google_reviews"];
|
|
6
|
+
export type DocumentedModuleKey = (typeof DOCUMENTED_MODULE_KEYS)[number];
|
|
7
|
+
export declare const GENERIC_MODULE_VERBS: readonly ["list", "ls", "docs", "doc", "request-setup", "request_setup", "enable", "disable", "archive", "config"];
|
|
8
|
+
export type ModuleCommandDef = {
|
|
9
|
+
key: DocumentedModuleKey;
|
|
10
|
+
name: string;
|
|
11
|
+
summary: string;
|
|
12
|
+
genericActions: readonly string[];
|
|
13
|
+
extraActions: readonly string[];
|
|
14
|
+
notYet: readonly string[];
|
|
15
|
+
examples: readonly string[];
|
|
16
|
+
notes: readonly string[];
|
|
17
|
+
};
|
|
18
|
+
export declare const MODULE_COMMANDS: readonly ModuleCommandDef[];
|
|
19
|
+
export declare const GOOGLE_REVIEWS_CLI_ACTIONS: readonly ["connect", "resolve", "status", "list-reviews", "hide", "show", "set-stars", "set-sort", "set-weight", "reset-weights", "write-review-url", "refresh", "disconnect"];
|
|
20
|
+
export declare const TYPED_CONFIG_CLI_ACTIONS: readonly ["get", "set"];
|
|
21
|
+
export declare function isCliModuleAction(key: DocumentedModuleKey, action: string): boolean;
|
|
22
|
+
export declare function isDocumentedModuleKey(raw: string): raw is DocumentedModuleKey;
|
|
23
|
+
export declare function isGenericModuleVerb(raw: string): boolean;
|
|
24
|
+
export declare function moduleCommand(key: DocumentedModuleKey): ModuleCommandDef;
|
|
25
|
+
export declare function formatModuleUsage(key: DocumentedModuleKey): string;
|
|
26
|
+
export declare function formatModulesIndex(): string;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §CLI-MOD-FW — per-key CLI registry (usage printer + help --json).
|
|
3
|
+
* Instagram is not documented here. Google reviews extras ship in Phase 1.
|
|
4
|
+
*/
|
|
5
|
+
export const DOCUMENTED_MODULE_KEYS = [
|
|
6
|
+
"callbar",
|
|
7
|
+
"forms",
|
|
8
|
+
"booking",
|
|
9
|
+
"signups",
|
|
10
|
+
"payments",
|
|
11
|
+
"shop",
|
|
12
|
+
"ordering",
|
|
13
|
+
"articles",
|
|
14
|
+
"google_reviews",
|
|
15
|
+
];
|
|
16
|
+
export const GENERIC_MODULE_VERBS = [
|
|
17
|
+
"list",
|
|
18
|
+
"ls",
|
|
19
|
+
"docs",
|
|
20
|
+
"doc",
|
|
21
|
+
"request-setup",
|
|
22
|
+
"request_setup",
|
|
23
|
+
"enable",
|
|
24
|
+
"disable",
|
|
25
|
+
"archive",
|
|
26
|
+
"config",
|
|
27
|
+
];
|
|
28
|
+
const GENERIC_ACTIONS = [
|
|
29
|
+
"docs",
|
|
30
|
+
"request-setup",
|
|
31
|
+
"enable",
|
|
32
|
+
"disable",
|
|
33
|
+
"archive",
|
|
34
|
+
"config",
|
|
35
|
+
];
|
|
36
|
+
export const MODULE_COMMANDS = [
|
|
37
|
+
{
|
|
38
|
+
key: "callbar",
|
|
39
|
+
name: "Call bar",
|
|
40
|
+
summary: "Phone / SMS bar. Typed get/set for phone, sms, and label.",
|
|
41
|
+
genericActions: GENERIC_ACTIONS,
|
|
42
|
+
extraActions: [
|
|
43
|
+
"get",
|
|
44
|
+
"set --phone +1… [--sms yes|no] [--label \"Call\"]",
|
|
45
|
+
],
|
|
46
|
+
notYet: [],
|
|
47
|
+
examples: [
|
|
48
|
+
"frontpage modules callbar",
|
|
49
|
+
"frontpage modules callbar get",
|
|
50
|
+
"frontpage modules callbar set --phone +14085551234 --sms no",
|
|
51
|
+
"frontpage modules callbar docs",
|
|
52
|
+
`frontpage modules config callbar --config '{"phone":"+1…"}'`,
|
|
53
|
+
],
|
|
54
|
+
notes: [
|
|
55
|
+
"Typed flags write the same whitelist as `modules config`. Generic JSON config still works.",
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "forms",
|
|
60
|
+
name: "Forms",
|
|
61
|
+
summary: "Lead forms. Get lists instances; set requires --instance.",
|
|
62
|
+
genericActions: GENERIC_ACTIONS,
|
|
63
|
+
extraActions: [
|
|
64
|
+
"get [--instance <slug>]",
|
|
65
|
+
"set --instance <slug> [--notify-email] [--confirm yes|no]",
|
|
66
|
+
],
|
|
67
|
+
notYet: [],
|
|
68
|
+
examples: [
|
|
69
|
+
"frontpage modules forms",
|
|
70
|
+
"frontpage modules forms get",
|
|
71
|
+
"frontpage modules forms get --instance lead-gen",
|
|
72
|
+
"frontpage modules forms set --instance lead-gen --notify-email hi@example.com",
|
|
73
|
+
"frontpage modules forms docs",
|
|
74
|
+
`frontpage modules config forms --instance lead-gen --config '{"notifyEmail":"hi@example.com"}'`,
|
|
75
|
+
],
|
|
76
|
+
notes: [
|
|
77
|
+
"Without --instance, get lists each form (id, status, notifyEmail). Do not target `default`.",
|
|
78
|
+
"Confirmation emails require a paid plan (same as Modules).",
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: "booking",
|
|
83
|
+
name: "Booking",
|
|
84
|
+
summary: "Booking requests. Typed get/set for notify email and hours.",
|
|
85
|
+
genericActions: GENERIC_ACTIONS,
|
|
86
|
+
extraActions: [
|
|
87
|
+
"get",
|
|
88
|
+
"set [--notify-email] [--hours \"…\"]",
|
|
89
|
+
],
|
|
90
|
+
notYet: [],
|
|
91
|
+
examples: [
|
|
92
|
+
"frontpage modules booking",
|
|
93
|
+
"frontpage modules booking get",
|
|
94
|
+
"frontpage modules booking set --notify-email book@example.com --hours \"Mon–Fri 9–5\"",
|
|
95
|
+
"frontpage modules booking docs",
|
|
96
|
+
],
|
|
97
|
+
notes: [
|
|
98
|
+
"Typed flags write the same whitelist as `modules config`. Generic JSON config still works.",
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
key: "signups",
|
|
103
|
+
name: "Mailing list",
|
|
104
|
+
summary: "Email signups. Typed get/set for notify email and confirmations.",
|
|
105
|
+
genericActions: GENERIC_ACTIONS,
|
|
106
|
+
extraActions: [
|
|
107
|
+
"get",
|
|
108
|
+
"set [--notify-email] [--confirm yes|no]",
|
|
109
|
+
],
|
|
110
|
+
notYet: [],
|
|
111
|
+
examples: [
|
|
112
|
+
"frontpage modules signups",
|
|
113
|
+
"frontpage modules signups get",
|
|
114
|
+
"frontpage modules signups set --notify-email hello@example.com --confirm no",
|
|
115
|
+
"frontpage modules signups docs",
|
|
116
|
+
],
|
|
117
|
+
notes: [
|
|
118
|
+
"Confirmation emails require a paid plan (same as Modules).",
|
|
119
|
+
"Typed flags write the same whitelist as `modules config`. Generic JSON config still works.",
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
key: "payments",
|
|
124
|
+
name: "Payments",
|
|
125
|
+
summary: "Stripe payments. Connect finishes in the browser, not the terminal.",
|
|
126
|
+
genericActions: GENERIC_ACTIONS,
|
|
127
|
+
extraActions: [],
|
|
128
|
+
notYet: ["connect", "disconnect", "status"],
|
|
129
|
+
examples: [
|
|
130
|
+
"frontpage modules payments",
|
|
131
|
+
"frontpage modules payments docs",
|
|
132
|
+
'frontpage modules payments request-setup --reason "Deposit button needs Stripe"',
|
|
133
|
+
],
|
|
134
|
+
notes: [
|
|
135
|
+
"OAuth cannot finish in this CLI. Use request-setup (editor amber bar) or the Modules hub.",
|
|
136
|
+
"payments and shop share one Stripe account.",
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
key: "shop",
|
|
141
|
+
name: "Online store",
|
|
142
|
+
summary: "Stripe shop. Shares the payments Stripe account.",
|
|
143
|
+
genericActions: GENERIC_ACTIONS,
|
|
144
|
+
extraActions: [],
|
|
145
|
+
notYet: ["connect", "disconnect", "status"],
|
|
146
|
+
examples: [
|
|
147
|
+
"frontpage modules shop",
|
|
148
|
+
"frontpage modules shop docs",
|
|
149
|
+
'frontpage modules shop request-setup --reason "You asked for a store — Stripe setup makes checkout live"',
|
|
150
|
+
],
|
|
151
|
+
notes: [
|
|
152
|
+
"OAuth cannot finish in this CLI. Use request-setup (editor amber bar) or the Modules hub.",
|
|
153
|
+
"payments and shop share one Stripe account.",
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
key: "ordering",
|
|
158
|
+
name: "Online ordering",
|
|
159
|
+
summary: "Square ordering. Connect finishes in the browser, not the terminal.",
|
|
160
|
+
genericActions: GENERIC_ACTIONS,
|
|
161
|
+
extraActions: [],
|
|
162
|
+
notYet: ["connect", "disconnect", "sync", "status"],
|
|
163
|
+
examples: [
|
|
164
|
+
"frontpage modules ordering",
|
|
165
|
+
"frontpage modules ordering docs",
|
|
166
|
+
'frontpage modules ordering request-setup --reason "You asked for online ordering — Square setup makes it live"',
|
|
167
|
+
],
|
|
168
|
+
notes: [
|
|
169
|
+
"OAuth cannot finish in this CLI. Use request-setup (editor amber bar) or the Modules hub.",
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
key: "articles",
|
|
174
|
+
name: "Articles",
|
|
175
|
+
summary: "Content hub. Lifecycle lives on `frontpage articles`, not under modules.",
|
|
176
|
+
genericActions: GENERIC_ACTIONS,
|
|
177
|
+
extraActions: [],
|
|
178
|
+
notYet: [],
|
|
179
|
+
examples: [
|
|
180
|
+
"frontpage modules articles",
|
|
181
|
+
"frontpage modules docs articles",
|
|
182
|
+
"frontpage articles list",
|
|
183
|
+
],
|
|
184
|
+
notes: [
|
|
185
|
+
"Do not fold article create/edit under `modules articles`. Use `frontpage articles …`.",
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
key: "google_reviews",
|
|
190
|
+
name: "Google reviews",
|
|
191
|
+
summary: "Connect a listing, list cached reviews, and curate what the site shows.",
|
|
192
|
+
genericActions: GENERIC_ACTIONS,
|
|
193
|
+
extraActions: [
|
|
194
|
+
"connect --place-id <id> | --maps-url <url>",
|
|
195
|
+
"resolve --place-id <id> | --maps-url <url>",
|
|
196
|
+
"status",
|
|
197
|
+
"list-reviews [--limit 20] [--offset 0] [--stars 4,5] [--hidden]",
|
|
198
|
+
"hide --id <reviewId>",
|
|
199
|
+
"show --id <reviewId>",
|
|
200
|
+
"set-stars 4,5",
|
|
201
|
+
"set-sort newest|highest|lowest",
|
|
202
|
+
"set-weight --id <reviewId> --weight 1-1000",
|
|
203
|
+
"reset-weights",
|
|
204
|
+
"write-review-url",
|
|
205
|
+
"refresh",
|
|
206
|
+
"disconnect [--yes]",
|
|
207
|
+
],
|
|
208
|
+
notYet: [],
|
|
209
|
+
examples: [
|
|
210
|
+
"frontpage modules google_reviews",
|
|
211
|
+
"frontpage modules google_reviews connect --place-id ChIJ…",
|
|
212
|
+
"frontpage modules google_reviews list-reviews --stars 4,5",
|
|
213
|
+
"frontpage modules google_reviews hide --id author:123",
|
|
214
|
+
"frontpage modules google_reviews set-stars 4,5",
|
|
215
|
+
"frontpage modules google_reviews docs",
|
|
216
|
+
'frontpage modules google_reviews request-setup --reason "You asked for Google reviews — pick your listing so we show the right ones"',
|
|
217
|
+
"frontpage inbox google_reviews",
|
|
218
|
+
],
|
|
219
|
+
notes: [
|
|
220
|
+
"Owner supplies a Place ID or Maps URL. The server resolves it. No Autocomplete, no Places key in the CLI.",
|
|
221
|
+
"Do not write placeId through `modules config` — that skips resolve + seed.",
|
|
222
|
+
"Disconnect deletes the listing connection, not the review cache.",
|
|
223
|
+
"Ask-widget scores: frontpage inbox google_reviews.",
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
export const GOOGLE_REVIEWS_CLI_ACTIONS = [
|
|
228
|
+
"connect",
|
|
229
|
+
"resolve",
|
|
230
|
+
"status",
|
|
231
|
+
"list-reviews",
|
|
232
|
+
"hide",
|
|
233
|
+
"show",
|
|
234
|
+
"set-stars",
|
|
235
|
+
"set-sort",
|
|
236
|
+
"set-weight",
|
|
237
|
+
"reset-weights",
|
|
238
|
+
"write-review-url",
|
|
239
|
+
"refresh",
|
|
240
|
+
"disconnect",
|
|
241
|
+
];
|
|
242
|
+
export const TYPED_CONFIG_CLI_ACTIONS = ["get", "set"];
|
|
243
|
+
export function isCliModuleAction(key, action) {
|
|
244
|
+
if (key === "google_reviews") {
|
|
245
|
+
return GOOGLE_REVIEWS_CLI_ACTIONS.includes(action);
|
|
246
|
+
}
|
|
247
|
+
if (key === "callbar" || key === "forms" || key === "booking" || key === "signups") {
|
|
248
|
+
return TYPED_CONFIG_CLI_ACTIONS.includes(action);
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
export function isDocumentedModuleKey(raw) {
|
|
253
|
+
return DOCUMENTED_MODULE_KEYS.includes(raw);
|
|
254
|
+
}
|
|
255
|
+
export function isGenericModuleVerb(raw) {
|
|
256
|
+
return GENERIC_MODULE_VERBS.includes(raw);
|
|
257
|
+
}
|
|
258
|
+
export function moduleCommand(key) {
|
|
259
|
+
return MODULE_COMMANDS.find((c) => c.key === key);
|
|
260
|
+
}
|
|
261
|
+
export function formatModuleUsage(key) {
|
|
262
|
+
const def = moduleCommand(key);
|
|
263
|
+
const lines = [
|
|
264
|
+
`${def.key} — ${def.name}`,
|
|
265
|
+
def.summary,
|
|
266
|
+
"",
|
|
267
|
+
`Usage: frontpage modules ${def.key} <action> [flags] [--site <slug>] [--json]`,
|
|
268
|
+
` frontpage modules <generic> ${def.key} …`,
|
|
269
|
+
"",
|
|
270
|
+
"Actions (generic):",
|
|
271
|
+
" docs Markup / agent how-to",
|
|
272
|
+
" request-setup Amber bar in the editor (CLI cannot show it)",
|
|
273
|
+
" enable|disable|archive",
|
|
274
|
+
" config --config '{…}' Whitelisted JSON only",
|
|
275
|
+
];
|
|
276
|
+
if (def.extraActions.length) {
|
|
277
|
+
lines.push("", "Actions (this module):", ...def.extraActions.map((a) => ` ${a}`));
|
|
278
|
+
}
|
|
279
|
+
if (def.notYet.length) {
|
|
280
|
+
lines.push("", "Not in this CLI yet (use Modules or the editor):", ` ${def.notYet.join(", ")}`);
|
|
281
|
+
}
|
|
282
|
+
if (def.notes.length) {
|
|
283
|
+
lines.push("", ...def.notes);
|
|
284
|
+
}
|
|
285
|
+
lines.push("", "Examples:", ...def.examples.map((e) => ` ${e}`));
|
|
286
|
+
lines.push("", "See also: https://frontpage.host/developers/modules");
|
|
287
|
+
return lines.join("\n");
|
|
288
|
+
}
|
|
289
|
+
export function formatModulesIndex() {
|
|
290
|
+
const rows = MODULE_COMMANDS.map((c) => ` ${c.key.padEnd(16)} ${c.summary}`);
|
|
291
|
+
return [
|
|
292
|
+
"Module keys (see `frontpage modules <key>` for that key's CLI usage):",
|
|
293
|
+
...rows,
|
|
294
|
+
"",
|
|
295
|
+
"Generic: frontpage modules list|docs|request-setup|enable|disable|archive|config …",
|
|
296
|
+
"Articles content: frontpage articles …",
|
|
297
|
+
"Index: https://frontpage.host/developers/modules",
|
|
298
|
+
].join("\n");
|
|
299
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runGoogleReviews(slug: string, action: string, flags: string[]): Promise<number>;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §CLI-MOD-GR — google_reviews extra verbs.
|
|
3
|
+
*/
|
|
4
|
+
import { assertNoUnknownFlags, hasFlag, takeFlag } from "../argv.js";
|
|
5
|
+
import { EXIT_OK, LONG_MODULE_TIMEOUT_MS, v1Get, v1Post, } from "../http/client.js";
|
|
6
|
+
import { emitOk, emitUsage, isJson, requireYesOrTty } from "../runtime.js";
|
|
7
|
+
import { formatModuleUsage } from "./commands.js";
|
|
8
|
+
const USAGE = formatModuleUsage("google_reviews");
|
|
9
|
+
function pathFor(slug) {
|
|
10
|
+
return `/api/v1/sites/${encodeURIComponent(slug)}/modules/google_reviews`;
|
|
11
|
+
}
|
|
12
|
+
function takePlaceBody(work) {
|
|
13
|
+
const placeId = takeFlag(work, "--place-id");
|
|
14
|
+
const mapsUrl = takeFlag(work, "--maps-url");
|
|
15
|
+
assertNoUnknownFlags(work, "modules");
|
|
16
|
+
if (placeId && mapsUrl) {
|
|
17
|
+
return emitUsage("modules", "Pass --place-id or --maps-url, not both.\n\n" + USAGE);
|
|
18
|
+
}
|
|
19
|
+
if (!placeId && !mapsUrl) {
|
|
20
|
+
return emitUsage("modules", "Usage: frontpage modules google_reviews connect|resolve --place-id <id> | --maps-url <url>\n\n" + USAGE);
|
|
21
|
+
}
|
|
22
|
+
return { ...(placeId ? { placeId } : {}), ...(mapsUrl ? { mapsUrl } : {}) };
|
|
23
|
+
}
|
|
24
|
+
function clip(text, max = 80) {
|
|
25
|
+
const one = text.replace(/\s+/g, " ").trim();
|
|
26
|
+
if (one.length <= max)
|
|
27
|
+
return one;
|
|
28
|
+
return `${one.slice(0, max - 1)}…`;
|
|
29
|
+
}
|
|
30
|
+
function printReviews(rows) {
|
|
31
|
+
if (!rows.length) {
|
|
32
|
+
console.log("(no reviews)");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (const r of rows) {
|
|
36
|
+
const vis = r.visibility.padEnd(12);
|
|
37
|
+
const stars = `${Math.round(r.rating)}★`;
|
|
38
|
+
const quote = r.text ? ` “${clip(r.text)}”` : "";
|
|
39
|
+
console.log(`${vis} ${stars} ${r.authorName}${quote}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function runGoogleReviews(slug, action, flags) {
|
|
43
|
+
const work = [...flags];
|
|
44
|
+
if (action === "status" || action === "write-review-url") {
|
|
45
|
+
assertNoUnknownFlags(work, "modules");
|
|
46
|
+
const data = await v1Get(pathFor(slug));
|
|
47
|
+
if (isJson()) {
|
|
48
|
+
emitOk("modules", data);
|
|
49
|
+
return EXIT_OK;
|
|
50
|
+
}
|
|
51
|
+
const url = data.listing?.writeReviewUrl;
|
|
52
|
+
if (action === "write-review-url") {
|
|
53
|
+
if (!url)
|
|
54
|
+
return emitUsage("modules", "Connect a Google listing first.");
|
|
55
|
+
console.log(url);
|
|
56
|
+
return EXIT_OK;
|
|
57
|
+
}
|
|
58
|
+
const name = String(data.connection?.display?.businessName ?? "(not connected)");
|
|
59
|
+
const address = data.connection?.display?.address;
|
|
60
|
+
console.log(name);
|
|
61
|
+
if (address)
|
|
62
|
+
console.log(String(address));
|
|
63
|
+
console.log(`ready ${data.connection?.ready ? "yes" : "no"}`);
|
|
64
|
+
if (data.listing) {
|
|
65
|
+
console.log(`rating ${data.listing.rating ?? "—"} (${data.listing.reviewCount ?? 0} listing)`);
|
|
66
|
+
console.log(`cached ${data.listing.cachedCount ?? 0}`);
|
|
67
|
+
if (data.listing.newestReviewAt)
|
|
68
|
+
console.log(`newest ${data.listing.newestReviewAt}`);
|
|
69
|
+
}
|
|
70
|
+
if (data.curation) {
|
|
71
|
+
console.log(`stars ${(data.curation.stars ?? []).join(",")}`);
|
|
72
|
+
console.log(`sort ${data.curation.sort ?? "newest"}`);
|
|
73
|
+
console.log(`hidden ${data.curation.hiddenCount ?? 0}`);
|
|
74
|
+
}
|
|
75
|
+
if (url)
|
|
76
|
+
console.log(`write ${url}`);
|
|
77
|
+
return EXIT_OK;
|
|
78
|
+
}
|
|
79
|
+
if (action === "list-reviews") {
|
|
80
|
+
const limit = takeFlag(work, "--limit");
|
|
81
|
+
const offset = takeFlag(work, "--offset");
|
|
82
|
+
const stars = takeFlag(work, "--stars");
|
|
83
|
+
const hidden = hasFlag(work, "--hidden");
|
|
84
|
+
assertNoUnknownFlags(work, "modules");
|
|
85
|
+
const q = new URLSearchParams();
|
|
86
|
+
if (limit)
|
|
87
|
+
q.set("limit", limit);
|
|
88
|
+
if (offset)
|
|
89
|
+
q.set("offset", offset);
|
|
90
|
+
if (stars)
|
|
91
|
+
q.set("stars", stars);
|
|
92
|
+
if (hidden)
|
|
93
|
+
q.set("hidden", "1");
|
|
94
|
+
const qs = q.toString() ? `?${q.toString()}` : "";
|
|
95
|
+
const data = await v1Get(`${pathFor(slug)}/reviews${qs}`);
|
|
96
|
+
if (isJson()) {
|
|
97
|
+
emitOk("modules", data);
|
|
98
|
+
return EXIT_OK;
|
|
99
|
+
}
|
|
100
|
+
console.log(`${data.reviews.length} of ${data.total} (offset ${data.offset})`);
|
|
101
|
+
printReviews(data.reviews);
|
|
102
|
+
return EXIT_OK;
|
|
103
|
+
}
|
|
104
|
+
if (action === "connect" || action === "resolve") {
|
|
105
|
+
const body = takePlaceBody(work);
|
|
106
|
+
if (typeof body === "number")
|
|
107
|
+
return body;
|
|
108
|
+
const data = await v1Post(pathFor(slug), { action, ...body }, action === "connect" ? { timeoutMs: LONG_MODULE_TIMEOUT_MS } : undefined);
|
|
109
|
+
if (isJson()) {
|
|
110
|
+
emitOk("modules", data);
|
|
111
|
+
return EXIT_OK;
|
|
112
|
+
}
|
|
113
|
+
if (action === "resolve") {
|
|
114
|
+
console.log(`${data.businessName} — ${data.address} — ${data.placeId}`);
|
|
115
|
+
return EXIT_OK;
|
|
116
|
+
}
|
|
117
|
+
const name = String(data.businessName ?? "listing");
|
|
118
|
+
const n = data.reviewCountCached ?? 0;
|
|
119
|
+
const url = data.writeReviewUrl ? ` Write-a-review: ${data.writeReviewUrl}` : "";
|
|
120
|
+
console.log(`Connected ${name} — ${n} reviews cached.${url}`);
|
|
121
|
+
return EXIT_OK;
|
|
122
|
+
}
|
|
123
|
+
if (action === "refresh") {
|
|
124
|
+
assertNoUnknownFlags(work, "modules");
|
|
125
|
+
const data = await v1Post(pathFor(slug), { action: "refresh" }, { timeoutMs: LONG_MODULE_TIMEOUT_MS });
|
|
126
|
+
if (isJson()) {
|
|
127
|
+
emitOk("modules", data);
|
|
128
|
+
return EXIT_OK;
|
|
129
|
+
}
|
|
130
|
+
console.log(`Refreshed — ${data.reviewCount ?? 0} reviews cached.`);
|
|
131
|
+
return EXIT_OK;
|
|
132
|
+
}
|
|
133
|
+
if (action === "disconnect") {
|
|
134
|
+
assertNoUnknownFlags(work, "modules");
|
|
135
|
+
const refuse = requireYesOrTty("disconnect Google reviews");
|
|
136
|
+
if (refuse)
|
|
137
|
+
return emitUsage("modules", refuse);
|
|
138
|
+
const data = await v1Post(pathFor(slug), { action: "disconnect" });
|
|
139
|
+
if (isJson()) {
|
|
140
|
+
emitOk("modules", data);
|
|
141
|
+
return EXIT_OK;
|
|
142
|
+
}
|
|
143
|
+
console.log("Disconnected. Cached reviews were kept.");
|
|
144
|
+
return EXIT_OK;
|
|
145
|
+
}
|
|
146
|
+
if (action === "hide" || action === "show") {
|
|
147
|
+
const id = takeFlag(work, "--id");
|
|
148
|
+
assertNoUnknownFlags(work, "modules");
|
|
149
|
+
if (!id)
|
|
150
|
+
return emitUsage("modules", `Usage: frontpage modules google_reviews ${action} --id <reviewId>\n\n${USAGE}`);
|
|
151
|
+
const data = await v1Post(pathFor(slug), {
|
|
152
|
+
action: action === "hide" ? "hide_review" : "show_review",
|
|
153
|
+
reviewId: id,
|
|
154
|
+
});
|
|
155
|
+
if (isJson()) {
|
|
156
|
+
emitOk("modules", data);
|
|
157
|
+
return EXIT_OK;
|
|
158
|
+
}
|
|
159
|
+
console.log(action === "hide" ? `Hidden ${id}.` : `Shown ${id}.`);
|
|
160
|
+
return EXIT_OK;
|
|
161
|
+
}
|
|
162
|
+
if (action === "set-stars") {
|
|
163
|
+
const fromFlag = takeFlag(work, "--stars");
|
|
164
|
+
const raw = fromFlag ?? work.shift();
|
|
165
|
+
assertNoUnknownFlags(work, "modules");
|
|
166
|
+
if (!raw)
|
|
167
|
+
return emitUsage("modules", `Usage: frontpage modules google_reviews set-stars 4,5\n\n${USAGE}`);
|
|
168
|
+
const stars = raw.split(",").map((s) => Number(s.trim())).filter((n) => Number.isInteger(n));
|
|
169
|
+
const data = await v1Post(pathFor(slug), {
|
|
170
|
+
action: "set_star_filter",
|
|
171
|
+
stars,
|
|
172
|
+
});
|
|
173
|
+
if (isJson()) {
|
|
174
|
+
emitOk("modules", data);
|
|
175
|
+
return EXIT_OK;
|
|
176
|
+
}
|
|
177
|
+
console.log(`Star filter: ${stars.join(",")}`);
|
|
178
|
+
return EXIT_OK;
|
|
179
|
+
}
|
|
180
|
+
if (action === "set-sort") {
|
|
181
|
+
const sort = takeFlag(work, "--sort") ?? work.shift();
|
|
182
|
+
assertNoUnknownFlags(work, "modules");
|
|
183
|
+
if (!sort || !["newest", "highest", "lowest"].includes(sort)) {
|
|
184
|
+
return emitUsage("modules", `Usage: frontpage modules google_reviews set-sort newest|highest|lowest\n\n${USAGE}`);
|
|
185
|
+
}
|
|
186
|
+
const data = await v1Post(pathFor(slug), {
|
|
187
|
+
action: "set_review_sort",
|
|
188
|
+
sort,
|
|
189
|
+
});
|
|
190
|
+
if (isJson()) {
|
|
191
|
+
emitOk("modules", data);
|
|
192
|
+
return EXIT_OK;
|
|
193
|
+
}
|
|
194
|
+
console.log(`Sort: ${sort}`);
|
|
195
|
+
return EXIT_OK;
|
|
196
|
+
}
|
|
197
|
+
if (action === "set-weight") {
|
|
198
|
+
const id = takeFlag(work, "--id");
|
|
199
|
+
const weightRaw = takeFlag(work, "--weight");
|
|
200
|
+
assertNoUnknownFlags(work, "modules");
|
|
201
|
+
if (!id || weightRaw == null) {
|
|
202
|
+
return emitUsage("modules", `Usage: frontpage modules google_reviews set-weight --id <reviewId> --weight 1-1000\n\n${USAGE}`);
|
|
203
|
+
}
|
|
204
|
+
const weight = Number(weightRaw);
|
|
205
|
+
const data = await v1Post(pathFor(slug), {
|
|
206
|
+
action: "set_review_weight",
|
|
207
|
+
reviewId: id,
|
|
208
|
+
weight,
|
|
209
|
+
});
|
|
210
|
+
if (isJson()) {
|
|
211
|
+
emitOk("modules", data);
|
|
212
|
+
return EXIT_OK;
|
|
213
|
+
}
|
|
214
|
+
console.log(weight === 1 ? `Weight cleared for ${id}.` : `Weight ${weight} for ${id}.`);
|
|
215
|
+
return EXIT_OK;
|
|
216
|
+
}
|
|
217
|
+
if (action === "reset-weights") {
|
|
218
|
+
assertNoUnknownFlags(work, "modules");
|
|
219
|
+
const data = await v1Post(pathFor(slug), { action: "reset_review_weights" });
|
|
220
|
+
if (isJson()) {
|
|
221
|
+
emitOk("modules", data);
|
|
222
|
+
return EXIT_OK;
|
|
223
|
+
}
|
|
224
|
+
console.log("Weights reset.");
|
|
225
|
+
return EXIT_OK;
|
|
226
|
+
}
|
|
227
|
+
return emitUsage("modules", `${USAGE}\n\nUnknown action "${action}".`);
|
|
228
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const TYPED_CONFIG_CLI_KEYS: readonly ["callbar", "forms", "booking", "signups"];
|
|
2
|
+
export type TypedConfigCliKey = (typeof TYPED_CONFIG_CLI_KEYS)[number];
|
|
3
|
+
export declare function isTypedConfigCliKey(key: string): key is TypedConfigCliKey;
|
|
4
|
+
export declare function runTypedConfig(key: TypedConfigCliKey, slug: string, action: string, flags: string[]): Promise<number>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §CLI-MOD-CFG — typed get/set for callbar, forms, booking, signups.
|
|
3
|
+
*/
|
|
4
|
+
import { assertNoUnknownFlags, takeFlag } from "../argv.js";
|
|
5
|
+
import { EXIT_OK, v1Get, v1Post } from "../http/client.js";
|
|
6
|
+
import { emitOk, emitUsage, isJson } from "../runtime.js";
|
|
7
|
+
import { formatModuleUsage } from "./commands.js";
|
|
8
|
+
export const TYPED_CONFIG_CLI_KEYS = ["callbar", "forms", "booking", "signups"];
|
|
9
|
+
export function isTypedConfigCliKey(key) {
|
|
10
|
+
return TYPED_CONFIG_CLI_KEYS.includes(key);
|
|
11
|
+
}
|
|
12
|
+
function pathFor(slug, key) {
|
|
13
|
+
return `/api/v1/sites/${encodeURIComponent(slug)}/modules/${encodeURIComponent(key)}`;
|
|
14
|
+
}
|
|
15
|
+
function dash(v) {
|
|
16
|
+
if (v == null || v === "")
|
|
17
|
+
return "—";
|
|
18
|
+
return String(v);
|
|
19
|
+
}
|
|
20
|
+
function yesNo(v) {
|
|
21
|
+
if (v === true || v === "yes" || v === "true")
|
|
22
|
+
return "yes";
|
|
23
|
+
if (v === false || v === "no" || v === "false")
|
|
24
|
+
return "no";
|
|
25
|
+
return "—";
|
|
26
|
+
}
|
|
27
|
+
function printLine(label, value) {
|
|
28
|
+
console.log(`${label.padEnd(10)} ${value}`);
|
|
29
|
+
}
|
|
30
|
+
function printCallbar(config) {
|
|
31
|
+
printLine("phone", dash(config.phone));
|
|
32
|
+
printLine("sms", yesNo(config.sms));
|
|
33
|
+
printLine("label", dash(config.label));
|
|
34
|
+
}
|
|
35
|
+
function printBooking(config) {
|
|
36
|
+
printLine("notify", dash(config.notifyEmail));
|
|
37
|
+
printLine("hours", dash(config.hours));
|
|
38
|
+
}
|
|
39
|
+
function printConfirmable(config, confirmations) {
|
|
40
|
+
printLine("notify", dash(config.notifyEmail));
|
|
41
|
+
const on = config.confirmEnabled === true;
|
|
42
|
+
if (confirmations && !confirmations.available)
|
|
43
|
+
printLine("confirm", `${yesNo(on)} (coming soon)`);
|
|
44
|
+
else if (confirmations && !confirmations.paid)
|
|
45
|
+
printLine("confirm", `${yesNo(on)} (paid plan required)`);
|
|
46
|
+
else
|
|
47
|
+
printLine("confirm", yesNo(on));
|
|
48
|
+
}
|
|
49
|
+
function printFormsList(instances) {
|
|
50
|
+
if (!instances.length) {
|
|
51
|
+
console.log("(no forms)");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const i of instances) {
|
|
55
|
+
const id = i.instance ?? "";
|
|
56
|
+
const label = i.label && i.label !== id ? i.label : "";
|
|
57
|
+
const status = String(i.status ?? "—");
|
|
58
|
+
const email = typeof i.config?.notifyEmail === "string" ? i.config.notifyEmail : "(account email)";
|
|
59
|
+
console.log(`${id.padEnd(20)} ${status.padEnd(12)} ${label ? `${label} ` : ""}${email}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function parseYesNoFlag(raw, flag) {
|
|
63
|
+
if (raw == null)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (raw === "yes" || raw === "true")
|
|
66
|
+
return true;
|
|
67
|
+
if (raw === "no" || raw === "false")
|
|
68
|
+
return false;
|
|
69
|
+
throw Object.assign(new Error(`${flag} must be yes or no.`), { exitCode: 2 });
|
|
70
|
+
}
|
|
71
|
+
export async function runTypedConfig(key, slug, action, flags) {
|
|
72
|
+
const usage = formatModuleUsage(key);
|
|
73
|
+
const work = [...flags];
|
|
74
|
+
if (action === "get") {
|
|
75
|
+
const instanceId = takeFlag(work, "--instance");
|
|
76
|
+
assertNoUnknownFlags(work, "modules");
|
|
77
|
+
const q = instanceId ? `?instanceId=${encodeURIComponent(instanceId)}` : "";
|
|
78
|
+
const data = await v1Get(`${pathFor(slug, key)}${q}`);
|
|
79
|
+
if (isJson()) {
|
|
80
|
+
emitOk("modules", data);
|
|
81
|
+
return EXIT_OK;
|
|
82
|
+
}
|
|
83
|
+
if (key === "forms" && !instanceId) {
|
|
84
|
+
printFormsList(data.instances ?? []);
|
|
85
|
+
return EXIT_OK;
|
|
86
|
+
}
|
|
87
|
+
if (key === "callbar")
|
|
88
|
+
printCallbar(data.config ?? {});
|
|
89
|
+
else if (key === "booking")
|
|
90
|
+
printBooking(data.config ?? {});
|
|
91
|
+
else
|
|
92
|
+
printConfirmable(data.config ?? {}, data.confirmations);
|
|
93
|
+
return EXIT_OK;
|
|
94
|
+
}
|
|
95
|
+
if (action === "set") {
|
|
96
|
+
const instanceId = takeFlag(work, "--instance");
|
|
97
|
+
const phone = takeFlag(work, "--phone");
|
|
98
|
+
const sms = takeFlag(work, "--sms");
|
|
99
|
+
const label = takeFlag(work, "--label");
|
|
100
|
+
const notifyEmail = takeFlag(work, "--notify-email");
|
|
101
|
+
const hours = takeFlag(work, "--hours");
|
|
102
|
+
const confirmRaw = takeFlag(work, "--confirm");
|
|
103
|
+
assertNoUnknownFlags(work, "modules");
|
|
104
|
+
let confirmEnabled;
|
|
105
|
+
try {
|
|
106
|
+
confirmEnabled = parseYesNoFlag(confirmRaw, "--confirm");
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
return emitUsage("modules", `${err instanceof Error ? err.message : String(err)}\n\n${usage}`);
|
|
110
|
+
}
|
|
111
|
+
const body = { action: "set" };
|
|
112
|
+
if (instanceId)
|
|
113
|
+
body.instanceId = instanceId;
|
|
114
|
+
if (phone !== undefined)
|
|
115
|
+
body.phone = phone;
|
|
116
|
+
if (sms !== undefined)
|
|
117
|
+
body.sms = sms;
|
|
118
|
+
if (label !== undefined)
|
|
119
|
+
body.label = label;
|
|
120
|
+
if (notifyEmail !== undefined)
|
|
121
|
+
body.notifyEmail = notifyEmail;
|
|
122
|
+
if (hours !== undefined)
|
|
123
|
+
body.hours = hours;
|
|
124
|
+
if (confirmEnabled !== undefined)
|
|
125
|
+
body.confirmEnabled = confirmEnabled;
|
|
126
|
+
if (key === "forms" && !instanceId) {
|
|
127
|
+
return emitUsage("modules", `Usage: frontpage modules forms set --instance <slug> [--notify-email] [--confirm yes|no]\n\n${usage}`);
|
|
128
|
+
}
|
|
129
|
+
const data = await v1Post(pathFor(slug, key), body);
|
|
130
|
+
if (isJson()) {
|
|
131
|
+
emitOk("modules", data);
|
|
132
|
+
return EXIT_OK;
|
|
133
|
+
}
|
|
134
|
+
const updated = Array.isArray(data.updated) ? data.updated.join(", ") : "settings";
|
|
135
|
+
console.log(`Updated ${key}${instanceId ? ` ${instanceId}` : ""} — ${updated}.`);
|
|
136
|
+
const cfg = data.config ?? {};
|
|
137
|
+
if (key === "callbar")
|
|
138
|
+
printCallbar(cfg);
|
|
139
|
+
else if (key === "booking")
|
|
140
|
+
printBooking(cfg);
|
|
141
|
+
else
|
|
142
|
+
printConfirmable(cfg);
|
|
143
|
+
return EXIT_OK;
|
|
144
|
+
}
|
|
145
|
+
return emitUsage("modules", `${usage}\n\nUnknown action "${action}".`);
|
|
146
|
+
}
|
package/dist/registry.js
CHANGED
|
@@ -317,18 +317,37 @@ export const COMMANDS = [
|
|
|
317
317
|
{
|
|
318
318
|
name: "modules",
|
|
319
319
|
summary: "List, read docs, or mutate modules (secret masks only)",
|
|
320
|
-
usage: "frontpage modules [list|docs|request-setup|enable|disable|archive|config] <key> [--instance <id>] [--config '{…}'] [--site <slug>]",
|
|
320
|
+
usage: "frontpage modules [list|docs|request-setup|enable|disable|archive|config] <key> | frontpage modules <key> [action] [--instance <id>] [--config '{…}'] [--site <slug>]",
|
|
321
321
|
topics: ["agent", "content"],
|
|
322
322
|
examples: [
|
|
323
|
+
"frontpage modules google_reviews",
|
|
324
|
+
"frontpage modules google_reviews connect --place-id ChIJ…",
|
|
325
|
+
"frontpage modules google_reviews list-reviews --stars 4,5",
|
|
326
|
+
"frontpage modules google_reviews enable",
|
|
327
|
+
"frontpage modules callbar get",
|
|
328
|
+
"frontpage modules callbar set --phone +14085551234 --sms no",
|
|
329
|
+
"frontpage modules forms get",
|
|
330
|
+
"frontpage modules forms set --instance lead-gen --notify-email hi@example.com",
|
|
323
331
|
"frontpage modules docs forms",
|
|
332
|
+
"frontpage modules docs google_reviews",
|
|
324
333
|
"frontpage modules docs signups",
|
|
325
334
|
"frontpage modules list",
|
|
326
335
|
'frontpage modules request-setup payments --reason "You asked for a $50 deposit button — Stripe setup makes it live"',
|
|
336
|
+
'frontpage modules request-setup google_reviews --reason "You asked for Google reviews — pick your listing so we show the right ones"',
|
|
337
|
+
],
|
|
338
|
+
flags: [
|
|
339
|
+
"Bare `frontpage modules <key>` prints that key's CLI usage (exit 2) — not agentDocs",
|
|
340
|
+
"Key-first and verb-first both work: `modules google_reviews enable` = `modules enable google_reviews`",
|
|
341
|
+
"google_reviews extras: connect / resolve / status / list-reviews / hide / show / set-stars / set-sort / set-weight / reset-weights / refresh / disconnect / write-review-url",
|
|
342
|
+
"callbar/forms/booking/signups: typed get / set (same whitelist as `modules config`)",
|
|
343
|
+
"See https://frontpage.host/developers/modules and /developers/modules/google-reviews",
|
|
327
344
|
],
|
|
328
345
|
dangers: [
|
|
329
346
|
"Secret values are never returned — masks only",
|
|
330
347
|
"request-setup surfaces an owner prompt in the editor status bar (CLI cannot show it)",
|
|
348
|
+
"Do not write placeId through generic config — that skips listing resolve + seed",
|
|
331
349
|
],
|
|
350
|
+
seeAlso: ["articles", "inbox"],
|
|
332
351
|
},
|
|
333
352
|
{
|
|
334
353
|
name: "extensions",
|
|
@@ -360,7 +379,7 @@ export const COMMANDS = [
|
|
|
360
379
|
{
|
|
361
380
|
name: "inbox",
|
|
362
381
|
summary: "List leads / subscribers / orders",
|
|
363
|
-
usage: "frontpage inbox [leads|subscribers|orders|…] [--mark-read] [--csv] [--site <slug>]",
|
|
382
|
+
usage: "frontpage inbox [leads|subscribers|orders|google_reviews|…] [--mark-read] [--csv] [--site <slug>]",
|
|
364
383
|
topics: ["content"],
|
|
365
384
|
dangers: ["--mark-read is side-effecting with no confirm"],
|
|
366
385
|
},
|
|
@@ -552,6 +571,7 @@ export const TOPICS = {
|
|
|
552
571
|
frontpage analytics summary|query|funnel|timeseries|compare|sql …
|
|
553
572
|
frontpage pages
|
|
554
573
|
frontpage modules
|
|
574
|
+
frontpage modules <key> # that key's CLI usage
|
|
555
575
|
frontpage modules request-setup <key> --reason "…"
|
|
556
576
|
frontpage extensions
|
|
557
577
|
|
|
@@ -564,12 +584,14 @@ export const TOPICS = {
|
|
|
564
584
|
Do not publish unless asked.`,
|
|
565
585
|
content: `Content
|
|
566
586
|
frontpage media list|upload <file> # images/PDF/video; text/CSV = inline not library; sitePath first when stored
|
|
567
|
-
frontpage inbox [leads|subscribers|orders|…] [--mark-read] [--csv]
|
|
587
|
+
frontpage inbox [leads|subscribers|orders|google_reviews|…] [--mark-read] [--csv]
|
|
568
588
|
frontpage seo get [/path|--path /path] | seo set --path /
|
|
569
589
|
frontpage articles list|content|create-section|create-article|set-draft|…
|
|
570
590
|
frontpage fonts [list|search] [--slot …] [--query …]
|
|
571
591
|
frontpage tokens set --group … --key … --value …
|
|
572
|
-
frontpage modules list|docs|request-setup|enable|disable|archive|config <key>
|
|
592
|
+
frontpage modules list|docs|request-setup|enable|disable|archive|config <key>
|
|
593
|
+
frontpage modules <key> # that key's CLI usage (generic only in this phase)
|
|
594
|
+
# mailing list key: signups. Index: https://frontpage.host/developers/modules
|
|
573
595
|
frontpage extensions list|docs|enable|disable|config <key>
|
|
574
596
|
frontpage settings get|set
|
|
575
597
|
frontpage edit --path / --old "…" --new "…"
|
package/llms.txt
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- Fonts: frontpage fonts [--slot] [--query] → exact value → tokens set fonts.<role> → APPLY font-sans|serif|mono|display. Build bundles catalog faces from tokens.ts and/or CSS font-family contexts (not prose). Inline-only without tokens works for catalog faces after rebuild; prefer tokens+utilities for brand roles.
|
|
9
9
|
- Module/extension docs: frontpage modules docs <key> | frontpage extensions docs <key>
|
|
10
10
|
- Search flags: --regex --case-sensitive --whole-word --files-only --context-lines --max-results
|
|
11
|
-
- Modules setup: frontpage modules request-setup <key> --reason "…" (owner status bar; CLI cannot show it)
|
|
11
|
+
- Modules setup: frontpage modules request-setup <key> --reason "…" (owner status bar; CLI cannot show it). frontpage modules <key> prints that key's CLI usage. Index: https://frontpage.host/developers/modules. Typed config: frontpage modules callbar|forms|booking|signups get|set (forms set needs --instance; confirm emails paid-plan only). google_reviews: frontpage modules google_reviews connect --place-id | --maps-url (server resolve + seed; no Autocomplete; do not write placeId via config). Also status, list-reviews, hide/show, set-stars, set-sort, set-weight, reset-weights, refresh, disconnect. Docs: https://frontpage.host/developers/modules/google-reviews. Markup: data-fp-google-reviews / data-fp-google-ask + required data-fp-requires="google_reviews". Ask scores: frontpage inbox google_reviews.
|
|
12
12
|
- Analytics ops: frontpage analytics summary|query|funnel|timeseries|compare|sql (bare analytics = dashboard)
|
|
13
13
|
- Media: frontpage media upload <file> (images/PDF ≤25 MB, video ≤50 MB, SVG/text ≤4 MB); human prints sitePath first — use /__media/uploads/<slug>/… in markup. Text/CSV is inline only (not media library; --json omits full content).
|
|
14
14
|
- Prompt attachments: frontpage prompt "…" --attach <file|/__media/…|https://…> [--asset-url https://…] (same media caps as media upload)
|