frontpage-host 0.1.22 → 0.1.24
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 +5 -0
- package/PEOPLE.md +169 -0
- package/README.md +5 -0
- package/dist/commands/mail.js +3 -0
- package/dist/commands/people-workspace.d.ts +3 -0
- package/dist/commands/people-workspace.js +137 -0
- package/dist/commands/people.js +15 -0
- package/dist/commands/settings.js +1 -1
- package/dist/commands/shop.js +35 -35
- package/dist/modules/commands.js +6 -3
- package/dist/modules/typed_config.js +15 -1
- package/dist/registry.js +19 -6
- package/llms.txt +5 -0
- package/package.json +4 -3
- package/dist/bin-path.d.ts +0 -21
- package/dist/bin-path.js +0 -65
package/AGENTS.md
CHANGED
|
@@ -251,3 +251,8 @@ frontpage help site-building
|
|
|
251
251
|
frontpage help agent
|
|
252
252
|
frontpage doctor --json
|
|
253
253
|
```
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
## People groups, stages, rules, and imports
|
|
257
|
+
|
|
258
|
+
Use `frontpage people overview`, `groups`, `stages`, `membership`, `rules`, `attachments`, `import`, and `company`. Use `frontpage mail campaign` for one-off group/stage/list audiences in Emails. Every editor action is available through these commands or `frontpage people workspace OPERATION --file payload.json`. Full operations and examples: [PEOPLE.md](./PEOPLE.md); public guide: https://frontpage.host/developers/people. Automated emails select a saved template or series. Campaigns use their own drafts.
|
package/PEOPLE.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# People: groups, stages, rules, imports, and campaigns
|
|
2
|
+
|
|
3
|
+
Start with manage_people {operation:"overview"} and {operation:"sources"}. Results provide stable IDs. Do not invent IDs or identify a form by its display name. A form keeps its slug when renamed; a removed form needs a new rule source.
|
|
4
|
+
|
|
5
|
+
Call manage_people with {operation, payload:{...fields}}. For campaign operations add area:"campaign". The tool binds this site's identity and permission; never pass siteId or slug. All examples below are payloads. CLI equivalents: frontpage people workspace OPERATION --data 'JSON' --site SLUG --json, or --file payload.json. All operations also have named commands: frontpage people groups create, rules update, membership move, etc. Run frontpage help people for flags.
|
|
6
|
+
|
|
7
|
+
## Groups and stages
|
|
8
|
+
A person can belong to many groups and one stage per group. Manual groups keep membership until an action changes it. Conditions groups reevaluate saved facts, including time. Unknown facts do not remove people. Manual include/exclude overrides conditions; override:null returns to automatic membership. hold:true stops this person's automatic changes for that group. Hidden people are excluded from campaign audiences and automated emails.
|
|
9
|
+
|
|
10
|
+
- groups.list {archived:false}; groups.get {id}; groups.create {label:"Customers",color:"emerald",mode:"manual"}; groups.update {id,label,color,mode,conditions}; groups.archive/restore {id}.
|
|
11
|
+
- Conditions example: groups.create {label:"Repeat buyers",mode:"conditions",conditions:{field:"purchaseCount",op:"gte",value:2}}.
|
|
12
|
+
- groups.preview {id,personId} or {conditions,cursor}. Preview sends nothing. Page through nextCursor.
|
|
13
|
+
- stages.create {groupId,label:"New",position:0,color:"sky",terminal:false}; stages.update {id,groupId,label,position,color,terminal}; stages.archive/restore {id,groupId}.
|
|
14
|
+
- membership.get {personId}; membership.add/remove {personId,groupId}; membership.move {personId,fromGroupId,groupId}; membership.stage {personId,groupId,stageId}; membership.advance {personId,groupId}; membership.override {personId,groupId,override:"include"|"exclude"|null}; membership.hold {personId,groupId,hold:true|false}. reset:true starts again at the first stage when adding/moving. Otherwise prior stage is retained on reentry. Advance follows ordered nonterminal stages and stops at a terminal stage; select a terminal stage explicitly.
|
|
15
|
+
- person.permission {personId,allowed:true|false} records the owner's email-permission decision. A saved unsubscribe or suppression still wins. Membership or platform signup never grants permission.
|
|
16
|
+
|
|
17
|
+
Existing tags become manual groups. Existing statuses become stages in Customer progress. Migration keeps person IDs, history, notes, follow-ups, and hidden state, and sends no email.
|
|
18
|
+
|
|
19
|
+
## When / If / Then
|
|
20
|
+
rules.create/update {id?,name,trigger,conditions:null|CONDITION,actions:[ACTION],enabled:false|true}. Draft first, validate/preview, then enable. rules.list/get/enable/pause/archive {id}; rules.runs {ruleId?,cursor?,limit}; rules.retry {runId} or {ruleId}. Changed or paused rules cannot replay old runs. Temporary failures retry with backoff; permanent failures stay visible until corrected. Durable action receipts prevent duplicate sends on retries. Rule edits create a new version and do not replay past events. Membership cycles and conflicting stage targets are rejected.
|
|
21
|
+
|
|
22
|
+
WHEN events: person.added, person.updated, form.submitted (optional formSlug), booking.requested, signup.subscribed/resubscribed/unsubscribed, payment.paid/refunded, order.paid/refunded/fulfilled (order.paid accepts productId), square_order.paid, group.joined/left (groupId), stage.entered (groupId,stageId), group.elapsed (groupId,delayMinutes), stage.elapsed (groupId,stageId,delayMinutes). Use sources for available forms/products. The stored form slug and product itemId survive display-name changes. Missing sources fail validation instead of silently selecting something else.
|
|
23
|
+
|
|
24
|
+
IF accepts {all:[CONDITION,...]} or {any:[CONDITION,...]}, up to four levels/100 clauses. Leaf forms:
|
|
25
|
+
- {field:"email"|"phone"|"name",op:"is"|"is_not"|"contains"|"present",value?:text}
|
|
26
|
+
- {field:"lastSeenDays"|"purchaseCount"|"spentCents"|"lastPurchaseDays",op:"is"|"is_not"|"gte"|"lte"|"present",value?:integer}
|
|
27
|
+
- {field:"subscribed",op:"is"|"is_not",value:boolean}
|
|
28
|
+
- {groupId,stageId?,member:boolean}; {formSlug,submitted:boolean}.
|
|
29
|
+
|
|
30
|
+
THEN supports 1–10 ordered actions:
|
|
31
|
+
{type:"add_group"|"remove_group",groupId}; {type:"move_group",fromGroupId,groupId}; {type:"set_stage",groupId,stageId,fromStageIds?:[ids]}; {type:"advance_stage",groupId,fromStageIds?:[ids]}; {type:"set_follow_up",minutes}; {type:"clear_follow_up"}; {type:"send_template",templateId}; {type:"start_series",seriesId}; {type:"cancel_series",seriesId}.
|
|
32
|
+
Use fromStageIds to prevent late payment/form events from resetting later progress. Cancel series requires a group/stage trigger and affects only enrollments started for that group. Existing series enrollments remain once per email/series; starting again does not reset them.
|
|
33
|
+
|
|
34
|
+
Example: rules.create {name:"Contact leads",trigger:{event:"form.submitted",formSlug:"contact"},conditions:null,actions:[{type:"add_group",groupId:LEADS},{type:"set_follow_up",minutes:1440}],enabled:true}.
|
|
35
|
+
Example: rules.create {name:"Paid customers",trigger:{event:"order.paid"},conditions:{groupId:LEADS,member:true},actions:[{type:"move_group",fromGroupId:LEADS,groupId:CUSTOMERS}],enabled:true}.
|
|
36
|
+
Example: rules.create {name:"Follow up after two days",trigger:{event:"stage.elapsed",groupId:LEADS,stageId:NEW,delayMinutes:2880},conditions:{field:"subscribed",op:"is",value:true},actions:[{type:"send_template",templateId:TEMPLATE}],enabled:true}.
|
|
37
|
+
|
|
38
|
+
## Attach an email
|
|
39
|
+
attachments.create {groupId,stageId?,emailKind:"template"|"series",emailId,name?,conditions?,enabled?}. This creates an ordinary editable rule on group.joined or stage.entered. Use one action label: Attach an email to this group/stage. Choose a saved email template or active series. Automated emails never use campaigns or inline content. Create/edit templates and series through the existing mail commands/tools. Pause/archive the returned rule to detach it.
|
|
40
|
+
|
|
41
|
+
## CSV
|
|
42
|
+
import.preview {csv,mapping?,headersOnly?:true}; mapping maps email/phone/name/emailPermission to exact CSV headers. Example mapping:{email:"Email address",name:"Full name"}. RFC 4180 quoted commas/newlines, BOM and CRLF are supported. Files up to 5 MB and 10,000 people. Provide email and/or phone. Duplicate contacts collapse; conflicting email/phone matches are rejected rather than merged. Preview includes row errors and duplicates and sends nothing.
|
|
43
|
+
import.start {csv,mapping,groupIds?:[ids],requestKey:UUID}; import.get {id}; import.retry {id}. Reuse requestKey for the same import after a network error; never change its contents. The worker retains its row cursor, outcomes and errors. Retry only failed rows after fixing the identity conflict. Imports send no email. Optional emailPermission column must represent existing permission; it never reverses a saved withdrawal.
|
|
44
|
+
|
|
45
|
+
## One-off campaigns in Emails
|
|
46
|
+
People selects the audience; Emails owns drafting and sending. No template-to-campaign action exists. Create a one_off draft with mail, then use manage_people area:"campaign":
|
|
47
|
+
preview {audience}; audience {documentId,audience}; send {documentId,requestKey}; get {documentId}.
|
|
48
|
+
Audience: {personIds:[ids]} OR {groupIds:[ids]} OR {stageIds:[ids]} OR {filters:{q?,source?,formSlug?,followUp?,lastSeenDays?}}. Combine ID lists as a union, filters narrow the result. {filters:{}} selects all eligible visible people. Preview returns matched/eligible/noEmail/suppressed/permissionMissing counts. Send freezes the eligible recipients and content. It deduplicates email addresses; missing email, hidden contacts, missing permission, and unsubscribe/suppression are excluded. Delivery rechecks permission. Queuing and delivery use the existing email worker, shared provider pacing and site/platform caps. Reuse the same requestKey after an ambiguous send response to prevent sending twice.
|
|
49
|
+
CLI: frontpage mail campaign preview --file audience.json; campaign audience --id DRAFT --file audience.json; campaign send --id DRAFT --request-key UUID; campaign get --id DRAFT.
|
|
50
|
+
|
|
51
|
+
## Company account sync
|
|
52
|
+
company.get/set {enabled:true,groupId?} is restricted to a platform admin on landing. It backfills Frontpage accounts and adds later signups through a bounded saved cursor, with user ID deduplication. Backfill never fires rules; new accounts after sync activation can fire group entry rules, subject to email permission. It preserves owner edits and does not grant marketing permission. Ordinary tenants cannot query platform users. Pause with enabled:false. Existing contacts remain.
|
|
53
|
+
|
|
54
|
+
## CLI recipes
|
|
55
|
+
|
|
56
|
+
Run `frontpage people overview --site demo --json` first. Copy the returned group, stage, person, email, and series IDs into the examples below. IDs are UUIDs, except a form slug or product item ID. Quotes around a JSON body are shell quotes; the JSON itself uses double quotes.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
frontpage people groups create --label Leads --color sky --site demo --json
|
|
60
|
+
frontpage people groups list --site demo --json
|
|
61
|
+
frontpage people stages create --group GROUP_ID --label New --position 0 --site demo --json
|
|
62
|
+
frontpage people stages create --group GROUP_ID --label Working --position 1 --site demo --json
|
|
63
|
+
frontpage people stages create --group GROUP_ID --label Won --position 2 --terminal true --site demo --json
|
|
64
|
+
frontpage people membership add --person PERSON_ID --group GROUP_ID --site demo --json
|
|
65
|
+
frontpage people membership stage --person PERSON_ID --group GROUP_ID --stage STAGE_ID --site demo --json
|
|
66
|
+
frontpage people membership hold --person PERSON_ID --group GROUP_ID --hold true --site demo --json
|
|
67
|
+
frontpage people membership move --person PERSON_ID --from-group OLD_GROUP_ID --group NEW_GROUP_ID --site demo --json
|
|
68
|
+
frontpage people membership remove --person PERSON_ID --group GROUP_ID --site demo --json
|
|
69
|
+
frontpage people membership override --person PERSON_ID --group GROUP_ID --override auto --site demo --json
|
|
70
|
+
frontpage people list --group GROUP_ID --stage STAGE_ID --site demo --json
|
|
71
|
+
frontpage people export --group GROUP_ID --out leads.csv --site demo
|
|
72
|
+
frontpage people attachments create --group GROUP_ID --email-kind template --email-id TEMPLATE_ID --enabled true --site demo --json
|
|
73
|
+
frontpage people attachments create --group GROUP_ID --stage STAGE_ID --email-kind series --email-id SERIES_ID --enabled true --site demo --json
|
|
74
|
+
frontpage people sources --site demo --json
|
|
75
|
+
frontpage people rules create --file rule.json --site demo --json
|
|
76
|
+
frontpage people rules preview --file rule.json --person PERSON_ID --site demo --json
|
|
77
|
+
frontpage people rules update --id RULE_ID --file rule.json --site demo --json
|
|
78
|
+
frontpage people rules enable --id RULE_ID --site demo --json
|
|
79
|
+
frontpage people rules pause --id RULE_ID --site demo --json
|
|
80
|
+
frontpage people rules runs --rule RULE_ID --site demo --json
|
|
81
|
+
frontpage people rules retry --run RUN_ID --site demo --json
|
|
82
|
+
frontpage people person permission --person PERSON_ID --allowed true --site demo --json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Use this `rule.json`, replacing IDs and the form slug with values from `overview` and `sources`:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"name": "New contact leads",
|
|
90
|
+
"trigger": {"event": "form.submitted", "formSlug": "contact"},
|
|
91
|
+
"conditions": {"field": "email", "op": "present"},
|
|
92
|
+
"actions": [
|
|
93
|
+
{"type": "add_group", "groupId": "GROUP_ID"},
|
|
94
|
+
{"type": "set_follow_up", "minutes": 1440}
|
|
95
|
+
],
|
|
96
|
+
"enabled": false
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
A condition group example (`group.json`):
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{"label":"Repeat buyers","mode":"conditions","conditions":{"all":[{"field":"purchaseCount","op":"gte","value":2},{"field":"lastPurchaseDays","op":"lte","value":90}]}}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
frontpage people groups create --file group.json --site demo --json
|
|
108
|
+
frontpage people groups preview --file group.json --site demo --json
|
|
109
|
+
frontpage people groups update --id GROUP_ID --file group.json --site demo --json
|
|
110
|
+
frontpage people groups archive --id GROUP_ID --site demo --json
|
|
111
|
+
frontpage people groups restore --id GROUP_ID --site demo --json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
CSV mapping file (`mapping.json`):
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{"mapping":{"email":"Email address","name":"Full name","phone":"Phone","emailPermission":"Permission"},"groupIds":["GROUP_ID"]}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
frontpage people import preview --csv customers.csv --file mapping.json --site demo --json
|
|
122
|
+
frontpage people import start --csv customers.csv --file mapping.json --request-key REQUEST_UUID --site demo --json
|
|
123
|
+
frontpage people import get --id IMPORT_ID --site demo --json
|
|
124
|
+
frontpage people import retry --id IMPORT_ID --site demo --json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Permission values are true/false or yes/no. Leave permission blank when unknown. Phone-only contacts can enter groups but cannot receive email. CSV import runs in saved batches; inspect the result for row errors before assuming every record was accepted. Fix an existing identity conflict, then retry failed rows. To correct the CSV itself, preview and start a new import with a new request key. A retry never changes the original file.
|
|
128
|
+
|
|
129
|
+
Campaign audience file (`audience.json`):
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{"audience":{"groupIds":["GROUP_ID"]}}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Use `stageIds` instead for one stage, or `personIds` for selected people. To select the whole visible People list use `{"audience":{"filters":{}}}`. Create an email draft using the existing email builder/CLI; campaigns do not start from templates.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
frontpage mail email create --file draft.json --name "Customer update" --site demo --json
|
|
139
|
+
frontpage mail campaign preview --file audience.json --site demo --json
|
|
140
|
+
frontpage mail campaign audience --id DRAFT_ID --file audience.json --site demo --json
|
|
141
|
+
frontpage mail campaign send --id DRAFT_ID --request-key REQUEST_UUID --site demo --json
|
|
142
|
+
frontpage mail campaign get --id DRAFT_ID --site demo --json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Company sync is available only to a platform admin in the landing workspace:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
frontpage people company get --site landing --json
|
|
149
|
+
frontpage people company set --enabled true --site landing --json
|
|
150
|
+
frontpage people company set --enabled false --site landing --json
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## API and agent parity
|
|
154
|
+
|
|
155
|
+
The same application service handles editor actions, the public CLI, and the site agent. Use POST `/api/v1/sites/SLUG/people/workspace` with an API key and body `{"operation":"groups.create","label":"Leads"}`. Campaign operations use POST `/api/v1/sites/SLUG/mail/campaign` with `operation: preview|audience|send|get`. Read actions require viewer access; writes require editor access. Public CLI access still requires Scale, Enterprise, or Internal. People and Emails keep their existing plan checks. Tenant IDs come from authenticated access, never the JSON body.
|
|
156
|
+
|
|
157
|
+
The agent uses `get_people_docs` for the complete reference and `manage_people` for every workspace action. For example `{"operation":"membership.add","payload":{"personId":"PERSON_ID","groupId":"GROUP_ID"}}`; a campaign uses `{"area":"campaign","operation":"preview","payload":{"audience":{"groupIds":["GROUP_ID"]}}}`. The agent can create templates and series with existing Email tools, then attach their IDs. No step requires opening the UI.
|
|
158
|
+
|
|
159
|
+
## Timing, limits, and safe changes
|
|
160
|
+
|
|
161
|
+
The existing minute worker evaluates new events, saved imports, company account sync, dynamic membership, timed rules, and pending actions. A busy queue can take longer than one minute. Form labels can change without breaking rules: the saved form slug is the identity. Changing/removing that slug creates a different source; choose it explicitly and update the rule. Product titles can change; rules keep the product item ID. Rename groups/stages freely; rules keep their IDs. Archive rather than deleting history.
|
|
162
|
+
|
|
163
|
+
Rules act on events after activation, not historical submissions. Every real group entry or stage entry can start its attached email; a duplicate event or retry cannot send it twice. An existing series enrollment does not restart on reentry. A hold stops automatic group changes and its attachment; lifting it does not replay failed work automatically. Review the run and retry deliberately. A queued series continues unless a rule explicitly stops that group's series. Missing permission, unsubscribe, and suppression always stop automated delivery.
|
|
164
|
+
|
|
165
|
+
Temporary database/provider failures retry up to eight attempts with delays from one minute to one hour. Permanent errors remain visible with the failed action and a manual retry. Earlier completed actions retain their receipts. Paused, archived, or changed rules do not replay old work. Condition previews and import previews send nothing. Conditions groups reevaluate all contacts in rotating batches; they do not require browser listeners.
|
|
166
|
+
|
|
167
|
+
Campaign queuing uses batches of 100. Sending uses the existing send window, hourly/monthly/site caps, platform daily cap, kill switch, and a shared provider slot across workers. Status `queued` means messages reached the existing send queue, not that every email was delivered. `campaign get` reports the accepted, sent, pending, failed, and skipped work. An unsubscribe after audience selection still prevents delivery.
|
|
168
|
+
|
|
169
|
+
Legacy tag/status commands remain compatible with converted groups/stages. New integrations should use group/stage IDs; historical fields remain for older callers and exports. All existing person IDs, notes, follow-ups, hidden state, source events, and external CRM destination behavior remain intact.
|
package/README.md
CHANGED
|
@@ -55,3 +55,8 @@ Machine-readable: `frontpage help --json` and global `--json` / `--quiet` on com
|
|
|
55
55
|
## Agents
|
|
56
56
|
|
|
57
57
|
See [AGENTS.md](./AGENTS.md) and [llms.txt](./llms.txt) in this package.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
## People groups, stages, rules, and imports
|
|
61
|
+
|
|
62
|
+
Use `frontpage people overview`, `groups`, `stages`, `membership`, `rules`, `attachments`, `import`, and `company`. Use `frontpage mail campaign` for one-off group/stage/list audiences in Emails. Every editor action is available through these commands or `frontpage people workspace OPERATION --file payload.json`. Full operations and examples: [PEOPLE.md](./PEOPLE.md); public guide: https://frontpage.host/developers/people. Automated emails select a saved template or series. Campaigns use their own drafts.
|
package/dist/commands/mail.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runCampaign } from "./people-workspace.js";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { resolveSlug } from "../site.js";
|
|
3
4
|
import { CliHttpError, EXIT_OK, EXIT_USAGE, exitCodeForError, v1Delete, v1Get, v1Patch, v1Post, v1Put, } from "../http/client.js";
|
|
@@ -79,6 +80,8 @@ export async function runMail(args) {
|
|
|
79
80
|
try {
|
|
80
81
|
const { slug, rest } = await resolveSlug(args);
|
|
81
82
|
const group = rest[0];
|
|
83
|
+
if (group === "campaign")
|
|
84
|
+
return await runCampaign(slug, rest.slice(1));
|
|
82
85
|
const base = `/api/v1/sites/${encodeURIComponent(slug)}/mail`;
|
|
83
86
|
if (group === "settings") {
|
|
84
87
|
const sub = rest[1] ?? "get";
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { assertNoUnknownFlags, takeFlag } from "../argv.js";
|
|
3
|
+
import { CliHttpError, EXIT_OK, EXIT_USAGE, v1Post } from "../http/client.js";
|
|
4
|
+
import { emitOk, isJson } from "../runtime.js";
|
|
5
|
+
const AREAS = [
|
|
6
|
+
"groups",
|
|
7
|
+
"stages",
|
|
8
|
+
"membership",
|
|
9
|
+
"rules",
|
|
10
|
+
"import",
|
|
11
|
+
"attachments",
|
|
12
|
+
"person",
|
|
13
|
+
"company",
|
|
14
|
+
];
|
|
15
|
+
export function isPeopleWorkspaceCommand(sub) {
|
|
16
|
+
return (!!sub &&
|
|
17
|
+
(AREAS.includes(sub) ||
|
|
18
|
+
["overview", "sources", "audience", "workspace"].includes(sub)));
|
|
19
|
+
}
|
|
20
|
+
export async function runPeopleWorkspace(slug, args) {
|
|
21
|
+
const work = [...args], area = work.shift();
|
|
22
|
+
const operation = area === "workspace"
|
|
23
|
+
? work.shift()
|
|
24
|
+
: area === "overview" || area === "sources"
|
|
25
|
+
? area
|
|
26
|
+
: `${area}.${work.shift() ?? (area === "company" ? "get" : area === "audience" ? "preview" : "list")}`;
|
|
27
|
+
const file = takeFlag(work, "--file"), json = takeFlag(work, "--data");
|
|
28
|
+
if (file && json)
|
|
29
|
+
throw new CliHttpError("Use --file or --data, not both.", EXIT_USAGE, 0, "bad_request");
|
|
30
|
+
let payload = {};
|
|
31
|
+
if (file || json) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(file ? await readFile(file, "utf8") : json);
|
|
34
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
35
|
+
throw Error();
|
|
36
|
+
payload = parsed;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new CliHttpError("Provide a JSON object through --file or --data.", EXIT_USAGE, 0, "bad_request");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const [flag, key] of Object.entries({
|
|
43
|
+
"--id": "id",
|
|
44
|
+
"--name": "name",
|
|
45
|
+
"--label": "label",
|
|
46
|
+
"--group": "groupId",
|
|
47
|
+
"--stage": "stageId",
|
|
48
|
+
"--person": "personId",
|
|
49
|
+
"--from-group": "fromGroupId",
|
|
50
|
+
"--mode": "mode",
|
|
51
|
+
"--color": "color",
|
|
52
|
+
"--run": "runId",
|
|
53
|
+
"--rule": "ruleId",
|
|
54
|
+
"--email-id": "emailId",
|
|
55
|
+
"--email-kind": "emailKind",
|
|
56
|
+
"--override": "override",
|
|
57
|
+
"--cursor": "cursor",
|
|
58
|
+
"--request-key": "requestKey",
|
|
59
|
+
})) {
|
|
60
|
+
const v = takeFlag(work, flag);
|
|
61
|
+
if (v !== undefined)
|
|
62
|
+
payload[key] = key === "override" && v === "auto" ? null : v;
|
|
63
|
+
}
|
|
64
|
+
for (const [flag, key] of Object.entries({
|
|
65
|
+
"--position": "position",
|
|
66
|
+
"--limit": "limit",
|
|
67
|
+
})) {
|
|
68
|
+
const v = takeFlag(work, flag);
|
|
69
|
+
if (v !== undefined) {
|
|
70
|
+
const n = Number(v);
|
|
71
|
+
if (!Number.isInteger(n))
|
|
72
|
+
throw new CliHttpError(`${flag} requires an integer.`, EXIT_USAGE, 0, "bad_request");
|
|
73
|
+
payload[key] = n;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const [flag, key] of Object.entries({
|
|
77
|
+
"--enabled": "enabled",
|
|
78
|
+
"--hold": "hold",
|
|
79
|
+
"--terminal": "terminal",
|
|
80
|
+
"--allowed": "allowed",
|
|
81
|
+
"--archived": "archived",
|
|
82
|
+
"--reset": "reset",
|
|
83
|
+
})) {
|
|
84
|
+
const v = takeFlag(work, flag);
|
|
85
|
+
if (v !== undefined) {
|
|
86
|
+
if (v !== "true" && v !== "false")
|
|
87
|
+
throw new CliHttpError(`${flag} is true or false.`, EXIT_USAGE, 0, "bad_request");
|
|
88
|
+
payload[key] = v === "true";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const csv = takeFlag(work, "--csv");
|
|
92
|
+
if (csv)
|
|
93
|
+
payload.csv = await readFile(csv, "utf8");
|
|
94
|
+
if (operation === "import.start" && !payload.requestKey)
|
|
95
|
+
throw new CliHttpError("Provide --request-key with a stable UUID so retries reuse this import.", EXIT_USAGE, 0, "bad_request");
|
|
96
|
+
assertNoUnknownFlags(work, "people");
|
|
97
|
+
if (work.length || !operation)
|
|
98
|
+
throw new CliHttpError("Unexpected arguments. Run frontpage help people.", EXIT_USAGE, 0, "bad_request");
|
|
99
|
+
const result = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/people/workspace`, { ...payload, operation });
|
|
100
|
+
if (isJson())
|
|
101
|
+
emitOk("people", result);
|
|
102
|
+
else
|
|
103
|
+
console.log(JSON.stringify(result, null, 2));
|
|
104
|
+
return EXIT_OK;
|
|
105
|
+
}
|
|
106
|
+
export async function runCampaign(slug, args) {
|
|
107
|
+
const work = [...args], operation = work.shift() ?? "get", file = takeFlag(work, "--file"), data = takeFlag(work, "--data"), documentId = takeFlag(work, "--id"), requestKey = takeFlag(work, "--request-key");
|
|
108
|
+
if (file && data)
|
|
109
|
+
throw new CliHttpError("Use --file or --data.", EXIT_USAGE, 0, "bad_request");
|
|
110
|
+
let payload = {};
|
|
111
|
+
if (file || data) {
|
|
112
|
+
try {
|
|
113
|
+
payload = JSON.parse(file ? await readFile(file, "utf8") : data);
|
|
114
|
+
if (!payload || Array.isArray(payload) || typeof payload !== "object")
|
|
115
|
+
throw Error();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
throw new CliHttpError("Provide a JSON object.", EXIT_USAGE, 0, "bad_request");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
assertNoUnknownFlags(work, "mail");
|
|
122
|
+
if (work.length)
|
|
123
|
+
throw new CliHttpError("Unexpected campaign arguments.", EXIT_USAGE, 0, "bad_request");
|
|
124
|
+
const result = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/mail/campaign`, {
|
|
125
|
+
...payload,
|
|
126
|
+
operation,
|
|
127
|
+
...(documentId ? { documentId } : {}),
|
|
128
|
+
...(operation === "send"
|
|
129
|
+
? { requestKey: requestKey ?? payload.requestKey }
|
|
130
|
+
: {}),
|
|
131
|
+
});
|
|
132
|
+
if (isJson())
|
|
133
|
+
emitOk("mail", result);
|
|
134
|
+
else
|
|
135
|
+
console.log(JSON.stringify(result, null, 2));
|
|
136
|
+
return EXIT_OK;
|
|
137
|
+
}
|
package/dist/commands/people.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPeopleWorkspaceCommand, runPeopleWorkspace } from "./people-workspace.js";
|
|
1
2
|
import { writeFile } from "node:fs/promises";
|
|
2
3
|
import { resolveSlug } from "../site.js";
|
|
3
4
|
import { CliHttpError, EXIT_OK, EXIT_USAGE, apiBase, exitCodeForError, getBearerAuthHeader, httpErrorFromV1, v1Delete, v1Get, v1Patch, v1Post, } from "../http/client.js";
|
|
@@ -73,11 +74,15 @@ export async function runPeople(args) {
|
|
|
73
74
|
try {
|
|
74
75
|
const { slug, rest } = await resolveSlug(args);
|
|
75
76
|
const sub = rest[0];
|
|
77
|
+
if (isPeopleWorkspaceCommand(sub))
|
|
78
|
+
return await runPeopleWorkspace(slug, rest);
|
|
76
79
|
const base = `/api/v1/sites/${encodeURIComponent(slug)}/people`;
|
|
77
80
|
if (!sub || sub === "list") {
|
|
78
81
|
const work = sub === "list" ? rest.slice(1) : [...rest];
|
|
79
82
|
const q = takeFlag(work, "--q") ?? takeFlag(work, "--query");
|
|
80
83
|
const status = takeFlag(work, "--status");
|
|
84
|
+
const groupId = takeFlag(work, "--group");
|
|
85
|
+
const stageId = takeFlag(work, "--stage");
|
|
81
86
|
const source = takeFlag(work, "--source");
|
|
82
87
|
const formSlug = takeFlag(work, "--form-slug");
|
|
83
88
|
const followUp = takeFlag(work, "--follow-up");
|
|
@@ -91,6 +96,10 @@ export async function runPeople(args) {
|
|
|
91
96
|
const params = new URLSearchParams();
|
|
92
97
|
if (q)
|
|
93
98
|
params.set("q", q);
|
|
99
|
+
if (groupId)
|
|
100
|
+
params.set("groupId", groupId);
|
|
101
|
+
if (stageId)
|
|
102
|
+
params.set("stageId", stageId);
|
|
94
103
|
if (status)
|
|
95
104
|
params.set("status", status);
|
|
96
105
|
if (source)
|
|
@@ -313,6 +322,8 @@ export async function runPeople(args) {
|
|
|
313
322
|
const work = rest.slice(1);
|
|
314
323
|
const q = takeFlag(work, "--q") ?? takeFlag(work, "--query");
|
|
315
324
|
const status = takeFlag(work, "--status");
|
|
325
|
+
const groupId = takeFlag(work, "--group");
|
|
326
|
+
const stageId = takeFlag(work, "--stage");
|
|
316
327
|
const source = takeFlag(work, "--source");
|
|
317
328
|
const formSlug = takeFlag(work, "--form-slug");
|
|
318
329
|
const followUp = takeFlag(work, "--follow-up");
|
|
@@ -325,6 +336,10 @@ export async function runPeople(args) {
|
|
|
325
336
|
const params = new URLSearchParams();
|
|
326
337
|
if (q)
|
|
327
338
|
params.set("q", q);
|
|
339
|
+
if (groupId)
|
|
340
|
+
params.set("groupId", groupId);
|
|
341
|
+
if (stageId)
|
|
342
|
+
params.set("stageId", stageId);
|
|
328
343
|
if (status)
|
|
329
344
|
params.set("status", status);
|
|
330
345
|
if (source)
|
|
@@ -32,7 +32,7 @@ export async function runSettings(args) {
|
|
|
32
32
|
}
|
|
33
33
|
const buildModel = takeFlag(work, "--build-model");
|
|
34
34
|
if (buildModel !== undefined)
|
|
35
|
-
body.buildModel = buildModel;
|
|
35
|
+
body.buildModel = buildModel === "default" ? null : buildModel;
|
|
36
36
|
const brandName = takeFlag(work, "--brand-name");
|
|
37
37
|
const brandLogo = takeFlag(work, "--brand-logo");
|
|
38
38
|
if (work.includes("--brand-reset")) {
|
package/dist/commands/shop.js
CHANGED
|
@@ -164,7 +164,7 @@ export async function runShop(args) {
|
|
|
164
164
|
const group = rest[0];
|
|
165
165
|
const base = `/api/v1/sites/${encodeURIComponent(slug)}/shop`;
|
|
166
166
|
if (!group)
|
|
167
|
-
return emitUsage("
|
|
167
|
+
return emitUsage("store", USAGE);
|
|
168
168
|
if (group === "items") {
|
|
169
169
|
const sub = rest[1];
|
|
170
170
|
const work = rest.slice(2);
|
|
@@ -181,7 +181,7 @@ export async function runShop(args) {
|
|
|
181
181
|
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
182
182
|
const data = await v1Get(`${base}/items${suffix}`);
|
|
183
183
|
if (isJson()) {
|
|
184
|
-
emitOk("
|
|
184
|
+
emitOk("store", data);
|
|
185
185
|
return EXIT_OK;
|
|
186
186
|
}
|
|
187
187
|
const rows = data.items ?? [];
|
|
@@ -198,7 +198,7 @@ export async function runShop(args) {
|
|
|
198
198
|
assertNoUnknownFlags(work, "store");
|
|
199
199
|
const data = await v1Get(`${base}/items/${encodeURIComponent(itemId)}`);
|
|
200
200
|
if (isJson()) {
|
|
201
|
-
emitOk("
|
|
201
|
+
emitOk("store", data);
|
|
202
202
|
return EXIT_OK;
|
|
203
203
|
}
|
|
204
204
|
printItem(data.item);
|
|
@@ -271,7 +271,7 @@ export async function runShop(args) {
|
|
|
271
271
|
}
|
|
272
272
|
const data = await v1Post(`${base}/items`, body);
|
|
273
273
|
if (isJson()) {
|
|
274
|
-
emitOk("
|
|
274
|
+
emitOk("store", data);
|
|
275
275
|
return EXIT_OK;
|
|
276
276
|
}
|
|
277
277
|
printItem(data.item);
|
|
@@ -313,7 +313,7 @@ export async function runShop(args) {
|
|
|
313
313
|
}
|
|
314
314
|
const data = await v1Patch(`${base}/items/${encodeURIComponent(itemId)}`, body);
|
|
315
315
|
if (isJson()) {
|
|
316
|
-
emitOk("
|
|
316
|
+
emitOk("store", data);
|
|
317
317
|
return EXIT_OK;
|
|
318
318
|
}
|
|
319
319
|
printItem(data.item);
|
|
@@ -324,7 +324,7 @@ export async function runShop(args) {
|
|
|
324
324
|
assertNoUnknownFlags(work, "store");
|
|
325
325
|
const data = await v1Post(`${base}/items/${encodeURIComponent(itemId)}/archive`, {});
|
|
326
326
|
if (isJson()) {
|
|
327
|
-
emitOk("
|
|
327
|
+
emitOk("store", data);
|
|
328
328
|
return EXIT_OK;
|
|
329
329
|
}
|
|
330
330
|
printItem(data.item);
|
|
@@ -337,7 +337,7 @@ export async function runShop(args) {
|
|
|
337
337
|
assertNoUnknownFlags(work, "store");
|
|
338
338
|
const data = await v1Post(`${base}/items/${encodeURIComponent(itemId)}/activate`, { active: active ?? true });
|
|
339
339
|
if (isJson()) {
|
|
340
|
-
emitOk("
|
|
340
|
+
emitOk("store", data);
|
|
341
341
|
return EXIT_OK;
|
|
342
342
|
}
|
|
343
343
|
printItem(data.item);
|
|
@@ -349,7 +349,7 @@ export async function runShop(args) {
|
|
|
349
349
|
assertNoUnknownFlags(work, "store");
|
|
350
350
|
const data = await v1Put(`${base}/items/${encodeURIComponent(itemId)}/images`, { images: urls });
|
|
351
351
|
if (isJson()) {
|
|
352
|
-
emitOk("
|
|
352
|
+
emitOk("store", data);
|
|
353
353
|
return EXIT_OK;
|
|
354
354
|
}
|
|
355
355
|
printItem(data.item);
|
|
@@ -396,13 +396,13 @@ export async function runShop(args) {
|
|
|
396
396
|
}
|
|
397
397
|
const data = await v1Post(`${base}/items/${encodeURIComponent(itemId)}/stock`, body);
|
|
398
398
|
if (isJson()) {
|
|
399
|
-
emitOk("
|
|
399
|
+
emitOk("store", data);
|
|
400
400
|
return EXIT_OK;
|
|
401
401
|
}
|
|
402
402
|
printStock(data);
|
|
403
403
|
return EXIT_OK;
|
|
404
404
|
}
|
|
405
|
-
return emitUsage("
|
|
405
|
+
return emitUsage("store", USAGE);
|
|
406
406
|
}
|
|
407
407
|
if (group === "variants") {
|
|
408
408
|
const sub = rest[1];
|
|
@@ -429,7 +429,7 @@ export async function runShop(args) {
|
|
|
429
429
|
active,
|
|
430
430
|
});
|
|
431
431
|
if (isJson()) {
|
|
432
|
-
emitOk("
|
|
432
|
+
emitOk("store", data);
|
|
433
433
|
return EXIT_OK;
|
|
434
434
|
}
|
|
435
435
|
printItem(data.item);
|
|
@@ -460,7 +460,7 @@ export async function runShop(args) {
|
|
|
460
460
|
}
|
|
461
461
|
const data = await v1Patch(`${base}/items/${encodeURIComponent(itemId)}/variants/${encodeURIComponent(variantId)}`, body);
|
|
462
462
|
if (isJson()) {
|
|
463
|
-
emitOk("
|
|
463
|
+
emitOk("store", data);
|
|
464
464
|
return EXIT_OK;
|
|
465
465
|
}
|
|
466
466
|
printItem(data.item);
|
|
@@ -472,13 +472,13 @@ export async function runShop(args) {
|
|
|
472
472
|
assertNoUnknownFlags(work, "store");
|
|
473
473
|
const data = await v1Post(`${base}/items/${encodeURIComponent(itemId)}/variants/${encodeURIComponent(variantId)}/archive`, {});
|
|
474
474
|
if (isJson()) {
|
|
475
|
-
emitOk("
|
|
475
|
+
emitOk("store", data);
|
|
476
476
|
return EXIT_OK;
|
|
477
477
|
}
|
|
478
478
|
printItem(data.item);
|
|
479
479
|
return EXIT_OK;
|
|
480
480
|
}
|
|
481
|
-
return emitUsage("
|
|
481
|
+
return emitUsage("store", USAGE);
|
|
482
482
|
}
|
|
483
483
|
if (group === "categories") {
|
|
484
484
|
const sub = rest[1];
|
|
@@ -490,7 +490,7 @@ export async function runShop(args) {
|
|
|
490
490
|
const qs = archived ? "?archived=1" : "";
|
|
491
491
|
const data = await v1Get(`${base}/categories${qs}`);
|
|
492
492
|
if (isJson()) {
|
|
493
|
-
emitOk("
|
|
493
|
+
emitOk("store", data);
|
|
494
494
|
return EXIT_OK;
|
|
495
495
|
}
|
|
496
496
|
const rows = data.categories ?? [];
|
|
@@ -517,7 +517,7 @@ export async function runShop(args) {
|
|
|
517
517
|
label,
|
|
518
518
|
});
|
|
519
519
|
if (isJson()) {
|
|
520
|
-
emitOk("
|
|
520
|
+
emitOk("store", data);
|
|
521
521
|
return EXIT_OK;
|
|
522
522
|
}
|
|
523
523
|
console.log(`${data.category.slug} ${data.category.label}`);
|
|
@@ -532,7 +532,7 @@ export async function runShop(args) {
|
|
|
532
532
|
}
|
|
533
533
|
const data = await v1Patch(`${base}/categories/${encodeURIComponent(slug)}`, { label });
|
|
534
534
|
if (isJson()) {
|
|
535
|
-
emitOk("
|
|
535
|
+
emitOk("store", data);
|
|
536
536
|
return EXIT_OK;
|
|
537
537
|
}
|
|
538
538
|
console.log(`${data.category.slug} ${data.category.label}`);
|
|
@@ -543,13 +543,13 @@ export async function runShop(args) {
|
|
|
543
543
|
assertNoUnknownFlags(work, "store");
|
|
544
544
|
const data = await v1Post(`${base}/categories/${encodeURIComponent(slug)}/archive`, {});
|
|
545
545
|
if (isJson()) {
|
|
546
|
-
emitOk("
|
|
546
|
+
emitOk("store", data);
|
|
547
547
|
return EXIT_OK;
|
|
548
548
|
}
|
|
549
549
|
console.log(`${data.category.slug} archived`);
|
|
550
550
|
return EXIT_OK;
|
|
551
551
|
}
|
|
552
|
-
return emitUsage("
|
|
552
|
+
return emitUsage("store", USAGE);
|
|
553
553
|
}
|
|
554
554
|
if (group === "settings") {
|
|
555
555
|
const sub = rest[1] ?? "get";
|
|
@@ -558,7 +558,7 @@ export async function runShop(args) {
|
|
|
558
558
|
assertNoUnknownFlags(work, "store");
|
|
559
559
|
const data = await v1Get(`${base}/settings`);
|
|
560
560
|
if (isJson()) {
|
|
561
|
-
emitOk("
|
|
561
|
+
emitOk("store", data);
|
|
562
562
|
return EXIT_OK;
|
|
563
563
|
}
|
|
564
564
|
const s = data.settings ?? {};
|
|
@@ -614,7 +614,7 @@ export async function runShop(args) {
|
|
|
614
614
|
freeShippingOverCents: freeCents,
|
|
615
615
|
});
|
|
616
616
|
if (isJson())
|
|
617
|
-
emitOk("
|
|
617
|
+
emitOk("store", data);
|
|
618
618
|
else
|
|
619
619
|
console.log("Fulfillment saved.");
|
|
620
620
|
return EXIT_OK;
|
|
@@ -627,7 +627,7 @@ export async function runShop(args) {
|
|
|
627
627
|
}
|
|
628
628
|
const data = await v1Post(`${base}/settings/currency`, { currency });
|
|
629
629
|
if (isJson())
|
|
630
|
-
emitOk("
|
|
630
|
+
emitOk("store", data);
|
|
631
631
|
else
|
|
632
632
|
console.log("Currency saved.");
|
|
633
633
|
return EXIT_OK;
|
|
@@ -640,7 +640,7 @@ export async function runShop(args) {
|
|
|
640
640
|
}
|
|
641
641
|
const data = await v1Post(`${base}/settings/checkout`, { checkout });
|
|
642
642
|
if (isJson())
|
|
643
|
-
emitOk("
|
|
643
|
+
emitOk("store", data);
|
|
644
644
|
else
|
|
645
645
|
console.log(checkout === "instant" ? "Checkout is Pay now." : "Checkout is add to cart.");
|
|
646
646
|
return EXIT_OK;
|
|
@@ -654,12 +654,12 @@ export async function runShop(args) {
|
|
|
654
654
|
cents,
|
|
655
655
|
});
|
|
656
656
|
if (isJson())
|
|
657
|
-
emitOk("
|
|
657
|
+
emitOk("store", data);
|
|
658
658
|
else
|
|
659
659
|
console.log(off ? "Service fee off." : "Service fee saved.");
|
|
660
660
|
return EXIT_OK;
|
|
661
661
|
}
|
|
662
|
-
return emitUsage("
|
|
662
|
+
return emitUsage("store", USAGE);
|
|
663
663
|
}
|
|
664
664
|
if (group === "connect") {
|
|
665
665
|
const work = rest.slice(1);
|
|
@@ -668,7 +668,7 @@ export async function runShop(args) {
|
|
|
668
668
|
const data = await v1Post(`${base}/connect`, { action: "connect" });
|
|
669
669
|
const already = data.alreadyConnected === true;
|
|
670
670
|
if (isJson()) {
|
|
671
|
-
emitOk("
|
|
671
|
+
emitOk("store", data);
|
|
672
672
|
}
|
|
673
673
|
else {
|
|
674
674
|
const url = resolveOpenUrl(data.connectUrl);
|
|
@@ -688,7 +688,7 @@ export async function runShop(args) {
|
|
|
688
688
|
assertNoUnknownFlags(work, "store");
|
|
689
689
|
const data = await v1Get(`${base}/connect`);
|
|
690
690
|
if (isJson()) {
|
|
691
|
-
emitOk("
|
|
691
|
+
emitOk("store", data);
|
|
692
692
|
return EXIT_OK;
|
|
693
693
|
}
|
|
694
694
|
const connection = (data.connection ?? {});
|
|
@@ -707,10 +707,10 @@ export async function runShop(args) {
|
|
|
707
707
|
assertNoUnknownFlags(work, "store");
|
|
708
708
|
const refuse = requireYesOrTty("disconnect the store (shared Stripe account with payments)");
|
|
709
709
|
if (refuse)
|
|
710
|
-
return emitUsage("
|
|
710
|
+
return emitUsage("store", refuse);
|
|
711
711
|
const data = await v1Post(`${base}/connect`, { action: "disconnect" });
|
|
712
712
|
if (isJson()) {
|
|
713
|
-
emitOk("
|
|
713
|
+
emitOk("store", data);
|
|
714
714
|
return EXIT_OK;
|
|
715
715
|
}
|
|
716
716
|
console.log(typeof data.message === "string" ? data.message : "Disconnected.");
|
|
@@ -721,7 +721,7 @@ export async function runShop(args) {
|
|
|
721
721
|
assertNoUnknownFlags(work, "store");
|
|
722
722
|
const data = await v1Post(`${base}/import`, {});
|
|
723
723
|
if (isJson()) {
|
|
724
|
-
emitOk("
|
|
724
|
+
emitOk("store", data);
|
|
725
725
|
return EXIT_OK;
|
|
726
726
|
}
|
|
727
727
|
const imported = data.imported ?? [];
|
|
@@ -748,7 +748,7 @@ export async function runShop(args) {
|
|
|
748
748
|
assertNoUnknownFlags(work, "store");
|
|
749
749
|
const data = await v1Get(`${base}/${noun}`);
|
|
750
750
|
if (isJson()) {
|
|
751
|
-
emitOk("
|
|
751
|
+
emitOk("store", data);
|
|
752
752
|
return EXIT_OK;
|
|
753
753
|
}
|
|
754
754
|
const rows = data[noun] ?? [];
|
|
@@ -766,17 +766,17 @@ export async function runShop(args) {
|
|
|
766
766
|
assertNoUnknownFlags(work, "store");
|
|
767
767
|
const data = await v1Get(`${base}/${noun}/${encodeURIComponent(id)}`);
|
|
768
768
|
if (isJson()) {
|
|
769
|
-
emitOk("
|
|
769
|
+
emitOk("store", data);
|
|
770
770
|
return EXIT_OK;
|
|
771
771
|
}
|
|
772
772
|
console.log(JSON.stringify(data[noun === "orders" ? "order" : "payment"], null, 2));
|
|
773
773
|
return EXIT_OK;
|
|
774
774
|
}
|
|
775
|
-
return emitUsage("
|
|
775
|
+
return emitUsage("store", USAGE);
|
|
776
776
|
}
|
|
777
|
-
return emitUsage("
|
|
777
|
+
return emitUsage("store", USAGE);
|
|
778
778
|
}
|
|
779
779
|
catch (err) {
|
|
780
|
-
return emitErr("
|
|
780
|
+
return emitErr("store", err);
|
|
781
781
|
}
|
|
782
782
|
}
|
package/dist/modules/commands.js
CHANGED
|
@@ -34,17 +34,18 @@ export const MODULE_COMMANDS = [
|
|
|
34
34
|
{
|
|
35
35
|
key: "callbar",
|
|
36
36
|
name: "Call bar",
|
|
37
|
-
summary: "Phone / SMS bar. Typed get/set for phone, sms, and label.",
|
|
37
|
+
summary: "Phone / SMS bar. Typed get/set for phone, sms, call, and label.",
|
|
38
38
|
genericActions: GENERIC_ACTIONS,
|
|
39
39
|
extraActions: [
|
|
40
40
|
"get",
|
|
41
|
-
"set --phone +1… [--sms yes|no] [--label \"Call\"]",
|
|
41
|
+
"set --phone +1… [--sms yes|no] [--call yes|no] [--label \"Call\"]",
|
|
42
42
|
],
|
|
43
43
|
notYet: [],
|
|
44
44
|
examples: [
|
|
45
45
|
"frontpage modules callbar",
|
|
46
46
|
"frontpage modules callbar get",
|
|
47
47
|
"frontpage modules callbar set --phone +14085551234 --sms no",
|
|
48
|
+
"frontpage modules callbar set --call no --sms yes",
|
|
48
49
|
"frontpage modules callbar docs",
|
|
49
50
|
`frontpage modules config callbar --config '{"phone":"+1…"}'`,
|
|
50
51
|
],
|
|
@@ -59,7 +60,7 @@ export const MODULE_COMMANDS = [
|
|
|
59
60
|
genericActions: GENERIC_ACTIONS,
|
|
60
61
|
extraActions: [
|
|
61
62
|
"get [--instance <slug>]",
|
|
62
|
-
"set --instance <slug> [--notify-email] [--confirm yes|no]",
|
|
63
|
+
"set --instance <slug> [--notify-email] [--confirm yes|no] [--confirm-template <id>]",
|
|
63
64
|
"destinations [--instance <slug>]",
|
|
64
65
|
"connect-destination <privyr|hubspot|pipedrive> --token \"…\" [--lead-source \"…\"]",
|
|
65
66
|
"attach-destination --instance <slug> --provider <id>",
|
|
@@ -74,6 +75,7 @@ export const MODULE_COMMANDS = [
|
|
|
74
75
|
"frontpage modules forms get",
|
|
75
76
|
"frontpage modules forms get --instance lead-gen",
|
|
76
77
|
"frontpage modules forms set --instance lead-gen --notify-email hi@example.com",
|
|
78
|
+
"frontpage modules forms set --instance lead-gen --confirm-template <template-id>",
|
|
77
79
|
"frontpage modules forms destinations",
|
|
78
80
|
"frontpage modules forms connect-destination privyr --token \"…\"",
|
|
79
81
|
"frontpage modules forms attach-destination --instance lead-gen --provider privyr",
|
|
@@ -83,6 +85,7 @@ export const MODULE_COMMANDS = [
|
|
|
83
85
|
notes: [
|
|
84
86
|
"Without --instance, get lists each form (id, status, notifyEmail). Do not target `default`.",
|
|
85
87
|
"Confirmation emails require a paid plan (same as Modules).",
|
|
88
|
+
"--confirm-template sets that form's saved template id. Every token must be one of the form's input names. An empty value clears it. A series is not attached to a form.",
|
|
86
89
|
"Destinations copy leads to HubSpot, Pipedrive, or Privyr. Connect once, then attach each form. Token write is admin (Privyr: paste the Incoming Webhook URL, not a token). request-destination-setup lights the editor amber bar (editor key). remove-destination requires --yes. Locations: https://frontpage.host/developers/modules#forms-destinations",
|
|
87
90
|
],
|
|
88
91
|
},
|
|
@@ -29,6 +29,7 @@ function printLine(label, value) {
|
|
|
29
29
|
}
|
|
30
30
|
function printCallbar(config) {
|
|
31
31
|
printLine("phone", dash(config.phone));
|
|
32
|
+
printLine("call", yesNo(config.call));
|
|
32
33
|
printLine("sms", yesNo(config.sms));
|
|
33
34
|
printLine("label", dash(config.label));
|
|
34
35
|
}
|
|
@@ -45,6 +46,9 @@ function printConfirmable(config, confirmations) {
|
|
|
45
46
|
printLine("confirm", `${yesNo(on)} (paid plan required)`);
|
|
46
47
|
else
|
|
47
48
|
printLine("confirm", yesNo(on));
|
|
49
|
+
if (typeof config.confirmTemplateId === "string") {
|
|
50
|
+
printLine("template", config.confirmTemplateId.trim() || "—");
|
|
51
|
+
}
|
|
48
52
|
}
|
|
49
53
|
function printFormsList(instances, destinations) {
|
|
50
54
|
if (!instances.length) {
|
|
@@ -108,10 +112,12 @@ export async function runTypedConfig(key, slug, action, flags) {
|
|
|
108
112
|
const instanceId = takeFlag(work, "--instance");
|
|
109
113
|
const phone = takeFlag(work, "--phone");
|
|
110
114
|
const sms = takeFlag(work, "--sms");
|
|
115
|
+
const call = takeFlag(work, "--call");
|
|
111
116
|
const label = takeFlag(work, "--label");
|
|
112
117
|
const notifyEmail = takeFlag(work, "--notify-email");
|
|
113
118
|
const hours = takeFlag(work, "--hours");
|
|
114
119
|
const confirmRaw = takeFlag(work, "--confirm");
|
|
120
|
+
const confirmTemplate = takeFlag(work, "--confirm-template");
|
|
115
121
|
assertNoUnknownFlags(work, "modules");
|
|
116
122
|
let confirmEnabled;
|
|
117
123
|
try {
|
|
@@ -127,6 +133,8 @@ export async function runTypedConfig(key, slug, action, flags) {
|
|
|
127
133
|
body.phone = phone;
|
|
128
134
|
if (sms !== undefined)
|
|
129
135
|
body.sms = sms;
|
|
136
|
+
if (call !== undefined)
|
|
137
|
+
body.call = call;
|
|
130
138
|
if (label !== undefined)
|
|
131
139
|
body.label = label;
|
|
132
140
|
if (notifyEmail !== undefined)
|
|
@@ -135,8 +143,14 @@ export async function runTypedConfig(key, slug, action, flags) {
|
|
|
135
143
|
body.hours = hours;
|
|
136
144
|
if (confirmEnabled !== undefined)
|
|
137
145
|
body.confirmEnabled = confirmEnabled;
|
|
146
|
+
if (confirmTemplate !== undefined) {
|
|
147
|
+
if (key !== "forms") {
|
|
148
|
+
return emitUsage("modules", `--confirm-template is only for forms.\n\n${usage}`);
|
|
149
|
+
}
|
|
150
|
+
body.confirmTemplateId = confirmTemplate.trim();
|
|
151
|
+
}
|
|
138
152
|
if (key === "forms" && !instanceId) {
|
|
139
|
-
return emitUsage("modules", `Usage: frontpage modules forms set --instance <slug> [--notify-email] [--confirm yes|no]\n\n${usage}`);
|
|
153
|
+
return emitUsage("modules", `Usage: frontpage modules forms set --instance <slug> [--notify-email] [--confirm yes|no] [--confirm-template <id>]\n\n${usage}`);
|
|
140
154
|
}
|
|
141
155
|
const data = await v1Post(pathFor(slug, key), body);
|
|
142
156
|
if (isJson()) {
|
package/dist/registry.js
CHANGED
|
@@ -333,8 +333,10 @@ export const COMMANDS = [
|
|
|
333
333
|
"frontpage modules google_reviews enable",
|
|
334
334
|
"frontpage modules callbar get",
|
|
335
335
|
"frontpage modules callbar set --phone +14085551234 --sms no",
|
|
336
|
+
"frontpage modules callbar set --call no --sms yes",
|
|
336
337
|
"frontpage modules forms get",
|
|
337
338
|
"frontpage modules forms set --instance lead-gen --notify-email hi@example.com",
|
|
339
|
+
"frontpage modules forms set --instance lead-gen --confirm-template <template-id>",
|
|
338
340
|
"frontpage store connect --no-open",
|
|
339
341
|
"frontpage modules docs forms",
|
|
340
342
|
"frontpage modules docs google_reviews",
|
|
@@ -437,10 +439,16 @@ export const COMMANDS = [
|
|
|
437
439
|
},
|
|
438
440
|
{
|
|
439
441
|
name: "people",
|
|
440
|
-
summary: "
|
|
441
|
-
usage: "frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide|tag|export
|
|
442
|
+
summary: "People, groups, stages, rules, imports, and campaign audiences",
|
|
443
|
+
usage: "frontpage people overview|sources|groups|stages|membership|rules|attachments|import|company|person|audience|workspace … [--site <slug>] [--json]; list|get|add|set|note|rm-note|follow-up|hide|unhide|tag|export remain available",
|
|
442
444
|
topics: ["content"],
|
|
443
445
|
examples: [
|
|
446
|
+
"frontpage people overview --json",
|
|
447
|
+
"frontpage people groups create --label Leads",
|
|
448
|
+
"frontpage people stages create --group GROUP_ID --label New --position 0",
|
|
449
|
+
"frontpage people membership add --person PERSON_ID --group GROUP_ID",
|
|
450
|
+
"frontpage people rules create --file rule.json",
|
|
451
|
+
"frontpage people import start --csv customers.csv --file mapping.json --request-key REQUEST_UUID",
|
|
444
452
|
"frontpage people list",
|
|
445
453
|
"frontpage people add --email alex@example.com --name Alex",
|
|
446
454
|
"frontpage people set <id> --status working",
|
|
@@ -453,6 +461,10 @@ export const COMMANDS = [
|
|
|
453
461
|
"frontpage people export",
|
|
454
462
|
],
|
|
455
463
|
flags: [
|
|
464
|
+
"Workspace: groups list|get|create|update|archive|restore|preview; stages create|update|archive|restore; membership get|add|remove|move|stage|advance|override|hold; rules list|get|create|update|enable|pause|archive|validate|preview|runs|retry; attachments create; import preview|start|get|retry; person permission; company get|set; audience preview; overview; sources",
|
|
465
|
+
"All workspace operations accept --file JSON or --data JSON. Named flags: --id --label --name --group --stage --person --from-group --mode --color --position --terminal --enabled --hold --allowed --archived --reset --override include|exclude|auto --email-kind template|series --email-id --rule --run --cursor --limit --request-key. Boolean flags take true|false. Complex conditions/actions/mappings use JSON.",
|
|
466
|
+
"CSV: --csv PATH plus --file mapping.json. import start requires a stable --request-key. Retry the same import with the same key and contents.",
|
|
467
|
+
"list/export: --group GROUP_ID --stage STAGE_ID. See package PEOPLE.md for full examples and every JSON shape.",
|
|
456
468
|
"list: --q --status --source --form-slug --follow-up due|upcoming|none --hidden --sort --last-seen-days --tag --limit --cursor",
|
|
457
469
|
"add requires --email and/or --phone (never name alone)",
|
|
458
470
|
"tag: --add <label> | --remove <slug>",
|
|
@@ -489,11 +501,11 @@ export const COMMANDS = [
|
|
|
489
501
|
"Integer cents only. $49.99 is 4999. Do not send 49.99 or a dollar string. --price-cents is the usual cost. Optional --discount-cents is the sale the buyer pays (strictly less than price). Omit --discount-cents when there is no sale. Checkout charges the sale; the site strikes through the usual price.",
|
|
490
502
|
"--add / --remove become signed delta. 5 adds 5. --remove 5 removes 5. Result copy is added 5 / removed 5 / set to 20 / stockNow.",
|
|
491
503
|
"Pass either --add, --remove, or --set. Never two together. --set unlimited returns counted stock to unlimited.",
|
|
492
|
-
"items: list|get|create|update|archive|activate [--off] | images --urls … | stock [--variant m] --add|--remove|--set. list --category hats. create/update --categories hats,summer",
|
|
504
|
+
"items: list|get|create|update|archive|activate [--off] | images --urls … | stock [--variant m] --add|--remove|--set. list --category hats. items list --json returns every product and every variant — do not paste that into a model. The editor manage_store list_items call is one page of 25 slim rows plus total; get_item is one product. create/update --categories hats,summer",
|
|
493
505
|
"categories: list|create|rename|archive. Create the category first. Assigning an unknown slug fails.",
|
|
494
506
|
"variants: add|update|archive <itemId> --id m --price-cents … [--option Size=M] [--stock]",
|
|
495
507
|
"settings: get|set-fulfillment|set-currency|set-checkout|set-checkout-fee. Currency is any Stripe presentment code such as usd or jpy. Checkout is store-wide (cart or instant). Do not use leftover items --checkout to pick cart vs Pay now. Gift cards still use --amount open --min-cents. Service fee applies to Add to cart and Pay now. Connect: frontpage store connect|status|disconnect. connect opens Frontpage then Stripe; --no-open for CI. disconnect keeps the shared Stripe account on payments.",
|
|
496
|
-
"Page markup is yours: data-fp-store-grid + data-fp-cart (cart starts hidden), every store control data-fp-requires=store. Product pages are the default: unless they said they do not want product pages, write one src/pages/products/item.astro (or shop) with data-fp-store-product. If they said no, skip that file and do not bind href or url. Description is a small markdown subset. PDP bind is <div data-fp-store-bind=description>. Do not bake titles or prices. Guide: https://frontpage.host/developers/store",
|
|
508
|
+
"Page markup is yours: data-fp-store-grid + data-fp-cart (cart starts hidden), every store control data-fp-requires=store. The public catalog pages with offset, limit, total, and truncated (default 24). list_items is the same shape with a max of 25. Product pages are the default: unless they said they do not want product pages, write one src/pages/products/item.astro (or shop) with data-fp-store-product. If they said no, skip that file and do not bind href or url. Description is a small markdown subset. PDP bind is <div data-fp-store-bind=description>. Do not bake titles or prices. Guide: https://frontpage.host/developers/store",
|
|
497
509
|
"import reads src/content/products/*.json from the site store into Store. One default variant each. Skip if that itemId already exists. Does not invent categories or change fulfillment.",
|
|
498
510
|
"orders / payments list|get read the same ledgers as frontpage inbox orders|payments",
|
|
499
511
|
"Unknown leftover flags fail closed (exit 2)",
|
|
@@ -502,8 +514,8 @@ export const COMMANDS = [
|
|
|
502
514
|
},
|
|
503
515
|
{
|
|
504
516
|
name: "mail",
|
|
505
|
-
summary: "
|
|
506
|
-
usage: "frontpage mail settings|email|template|series|send|sends|enrolments|suppress … [--site <slug>] [--json]",
|
|
517
|
+
summary: "Emails: campaign drafts, People audiences, templates, series, and sends",
|
|
518
|
+
usage: "frontpage mail settings|email|template|series|campaign|send|sends|enrolments|suppress … [--site <slug>] [--json]",
|
|
507
519
|
topics: ["content"],
|
|
508
520
|
examples: [
|
|
509
521
|
"frontpage mail settings set --postal \"100 Main St\" --reply-to you@example.com",
|
|
@@ -514,6 +526,7 @@ export const COMMANDS = [
|
|
|
514
526
|
"frontpage mail suppress add --email bounce@example.com",
|
|
515
527
|
],
|
|
516
528
|
flags: [
|
|
529
|
+
"campaign preview|audience|send|get. --id DRAFT_ID, --file audience.json ({audience:{groupIds|stageIds|personIds|filters}}). send requires --request-key; reuse it after a network error. Templates/series are for automation; campaigns have separate drafts. See PEOPLE.md.",
|
|
517
530
|
"email list is unsent drafts only. Sent one-offs are mail sends list",
|
|
518
531
|
"send --file is an inline one-off (no saved document)",
|
|
519
532
|
"Blocks and series steps are JSON (--file). Unknown leftover flags fail closed (exit 2)",
|
package/llms.txt
CHANGED
|
@@ -30,3 +30,8 @@
|
|
|
30
30
|
- Machine: global --json / --quiet; exit 0/2/3/4/5 (see help exit-codes)
|
|
31
31
|
- deploy wait = agent-run idle (if any) + tip compile ready; it is NOT go-live / publish
|
|
32
32
|
- No runs continue, domains, billing, members, MCP, or git remotes in v1
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## People groups, stages, rules, and imports
|
|
36
|
+
|
|
37
|
+
Use `frontpage people overview`, `groups`, `stages`, `membership`, `rules`, `attachments`, `import`, and `company`. Use `frontpage mail campaign` for one-off group/stage/list audiences in Emails. Every editor action is available through these commands or `frontpage people workspace OPERATION --file payload.json`. Full operations and examples: [PEOPLE.md](./PEOPLE.md); public guide: https://frontpage.host/developers/people. Automated emails select a saved template or series. Campaigns use their own drafts.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frontpage-host",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Frontpage CLI
|
|
3
|
+
"version": "0.1.24",
|
|
4
|
+
"description": "Frontpage CLI \u2014 manage your site from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"frontpage": "dist/bin.js"
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"dist",
|
|
11
11
|
"README.md",
|
|
12
12
|
"AGENTS.md",
|
|
13
|
-
"llms.txt"
|
|
13
|
+
"llms.txt",
|
|
14
|
+
"PEOPLE.md"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
16
17
|
"build": "tsc -p tsconfig.json",
|
package/dist/bin-path.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export type PathFrontpageVerdict = {
|
|
2
|
-
stale: boolean;
|
|
3
|
-
path: string | null;
|
|
4
|
-
pathVersion: string | null;
|
|
5
|
-
reason: "older-than-compile-wait" | "older-than-this-package" | "unreadable" | null;
|
|
6
|
-
};
|
|
7
|
-
export declare function resolveRealPath(path: string): string;
|
|
8
|
-
export declare function interpretPathFrontpage(args: {
|
|
9
|
-
thisVersion: string;
|
|
10
|
-
thisBin: string | null;
|
|
11
|
-
pathBin: string | null;
|
|
12
|
-
pathVersionLine: string | null;
|
|
13
|
-
minVersion?: string;
|
|
14
|
-
}): PathFrontpageVerdict;
|
|
15
|
-
export declare function whichFrontpage(): string | null;
|
|
16
|
-
export declare function frontpageVersionAt(bin: string): string | null;
|
|
17
|
-
export declare function inspectPathFrontpage(opts?: {
|
|
18
|
-
thisBin?: string | null;
|
|
19
|
-
thisVersion?: string;
|
|
20
|
-
}): PathFrontpageVerdict;
|
|
21
|
-
export declare function stalePathUpgradeMessage(v: PathFrontpageVerdict): string;
|
package/dist/bin-path.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Detect a stale `frontpage` on PATH (global 0.1.9 vs this package).
|
|
3
|
-
* Does not uninstall anything — doctor prints the uninstall command.
|
|
4
|
-
*/
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
|
-
import { realpathSync } from "node:fs";
|
|
7
|
-
import { COMPILE_WAIT_MIN_VERSION, isOlderThan, packageIdentity, parseVersionLine, } from "./version.js";
|
|
8
|
-
export function resolveRealPath(path) {
|
|
9
|
-
try {
|
|
10
|
-
return realpathSync(path);
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
return path;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
export function interpretPathFrontpage(args) {
|
|
17
|
-
if (!args.pathBin) {
|
|
18
|
-
return { stale: false, path: null, pathVersion: null, reason: null };
|
|
19
|
-
}
|
|
20
|
-
if (args.thisBin && resolveRealPath(args.pathBin) === resolveRealPath(args.thisBin)) {
|
|
21
|
-
return { stale: false, path: args.pathBin, pathVersion: args.thisVersion, reason: null };
|
|
22
|
-
}
|
|
23
|
-
const pathVersion = parseVersionLine(args.pathVersionLine ?? "");
|
|
24
|
-
if (!pathVersion) {
|
|
25
|
-
return { stale: true, path: args.pathBin, pathVersion: null, reason: "unreadable" };
|
|
26
|
-
}
|
|
27
|
-
const floor = args.minVersion ?? COMPILE_WAIT_MIN_VERSION;
|
|
28
|
-
if (isOlderThan(pathVersion, floor)) {
|
|
29
|
-
return { stale: true, path: args.pathBin, pathVersion, reason: "older-than-compile-wait" };
|
|
30
|
-
}
|
|
31
|
-
if (isOlderThan(pathVersion, args.thisVersion)) {
|
|
32
|
-
return { stale: true, path: args.pathBin, pathVersion, reason: "older-than-this-package" };
|
|
33
|
-
}
|
|
34
|
-
return { stale: false, path: args.pathBin, pathVersion, reason: null };
|
|
35
|
-
}
|
|
36
|
-
export function whichFrontpage() {
|
|
37
|
-
const r = spawnSync("sh", ["-c", "command -v frontpage"], { encoding: "utf8" });
|
|
38
|
-
const line = (r.stdout ?? "").trim().split("\n")[0]?.trim();
|
|
39
|
-
return line || null;
|
|
40
|
-
}
|
|
41
|
-
export function frontpageVersionAt(bin) {
|
|
42
|
-
const r = spawnSync(bin, ["--version"], { encoding: "utf8" });
|
|
43
|
-
const text = `${r.stdout ?? ""}\n${r.stderr ?? ""}`;
|
|
44
|
-
return parseVersionLine(text);
|
|
45
|
-
}
|
|
46
|
-
export function inspectPathFrontpage(opts) {
|
|
47
|
-
const thisVersion = opts?.thisVersion ?? packageIdentity().version;
|
|
48
|
-
const thisBin = opts?.thisBin ?? process.argv[1] ?? null;
|
|
49
|
-
const pathBin = whichFrontpage();
|
|
50
|
-
const pathVersionLine = pathBin ? frontpageVersionAt(pathBin) : null;
|
|
51
|
-
return interpretPathFrontpage({
|
|
52
|
-
thisVersion,
|
|
53
|
-
thisBin,
|
|
54
|
-
pathBin,
|
|
55
|
-
pathVersionLine,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
export function stalePathUpgradeMessage(v) {
|
|
59
|
-
const found = v.pathVersion ? `frontpage-host ${v.pathVersion}` : "an unknown frontpage-host";
|
|
60
|
-
const where = v.path ? ` at ${v.path}` : "";
|
|
61
|
-
return (`PATH frontpage is ${found}${where}. Compile-wait needs frontpage-host ${COMPILE_WAIT_MIN_VERSION} or newer. ` +
|
|
62
|
-
`Uninstall the stale global (npm uninstall -g frontpage-host) and install ` +
|
|
63
|
-
`npm i -g frontpage-host@${COMPILE_WAIT_MIN_VERSION} (or npx frontpage-host@${COMPILE_WAIT_MIN_VERSION}). ` +
|
|
64
|
-
`In the Frontpage repo, run node <repo>/cli/dist/bin.js instead of a global binary.`);
|
|
65
|
-
}
|