@letterstory/cli 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -229,6 +229,33 @@ letterstory onboarding status
229
229
  letterstory onboarding step --complete connect_domain
230
230
  ```
231
231
 
232
+ ### One-shot domain onboarding
233
+
234
+ A bare company domain in, a fully-configured organization out — as ONE durable job that
235
+ composes the existing engines: the main blog (imported from its sitemap or a connected
236
+ CMS, content areas, topic map, SEO, series, company GEO probing, headlines), org visuals
237
+ inferred from the site, the main blog's Enhance flow configured (new posts are enhanced;
238
+ imported posts are left as published), a phantom provisioned + configured + seeded with
239
+ posts that are actually written and published, and Seers in suggest mode when they fit.
240
+ The job finishes from a readback: it waits (default 2h) for the seed posts to publish and
241
+ the first GEO probe to land, then reports what landed.
242
+
243
+ ```bash
244
+ # On the org behind your key (its registered domain is the source):
245
+ letterstory phantom-onboard domain --about "payroll for platform teams" --posts 3 --watch
246
+ letterstory phantom-onboard domain --sitemap https://acme.com/sitemap.xml --seers skip --no-visuals
247
+ letterstory phantom-onboard domain --attach <job-id> --watch # watch a job started elsewhere
248
+ letterstory phantom-onboard domain --status # the org's latest domain onboarding
249
+
250
+ # Staff: create the org itself first (the key exchange), with a STAFF key.
251
+ LETTERSTORY_STAFF_API_KEY=<staff key with org:create> \
252
+ letterstory admin create-org --domain acme.com --onboard --about "payroll for platform teams"
253
+ # → prints the org id, a fresh ORG-scoped API key (once), and the job id to watch.
254
+ ```
255
+
256
+ Everything the parent job does is also its own tool for existing orgs:
257
+ `ensure_collection_enhance_flow`, `provision_org_visuals`, `propose_seers` (`--args '{"apply":true}'`).
258
+
232
259
  ## Content series
233
260
 
234
261
  A series is a coverage obligation, not a keyword bet: "content that looks like this",
package/lib/cli.mjs CHANGED
@@ -37,10 +37,11 @@ import {
37
37
  cmdTemplates,
38
38
  cmdShredder,
39
39
  cmdLandings,
40
+ cmdAdmin,
40
41
  } from "./commands.mjs";
41
42
 
42
43
  // Keep in sync with cli/package.json.
43
- export const VERSION = "0.12.0";
44
+ export const VERSION = "0.14.0";
44
45
 
45
46
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
46
47
  // can't accidentally swallow the id as --json's value.
@@ -64,9 +65,23 @@ const BOOLEAN_FLAGS = new Set([
64
65
  // both put a value-less flag next to a value: without these entries the parser
65
66
  // hands the following token to the flag and the real argument disappears.
66
67
  "regenerate",
68
+ // `connectors connect --provider framer --token … --auto-deploy` / `--no-auto-deploy`
69
+ // are value-less; without these the parser eats the following token.
70
+ "auto-deploy",
71
+ "no-auto-deploy",
67
72
  "stock-only",
68
73
  "gate",
69
74
  "skip",
75
+ // `phantom-onboard domain` / `admin create-org --onboard` switches.
76
+ "watch",
77
+ "status",
78
+ "no-phantom",
79
+ "no-visuals",
80
+ "no-enhance",
81
+ "no-key",
82
+ "onboard",
83
+ "test",
84
+ "real",
70
85
  ]);
71
86
 
72
87
  // Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
@@ -208,6 +223,16 @@ Connectors:
208
223
  collection, reconciling (adopt/update/create/orphan).
209
224
  --collection for a bound collection, --provider to
210
225
  auto-resolve, or both to wire an unbound collection.
226
+ connectors connect --provider <p> --token <api-token>
227
+ Save + validate CMS credentials. framer also needs
228
+ --project-url (+ [--no-auto-deploy]); sanity needs
229
+ --project-id [--dataset]; contentful needs --space-id
230
+ [--environment].
231
+ connectors disconnect --provider <p> Forget a CMS connection (keeps imported posts)
232
+ connectors connections Show connection status for every provider
233
+ connectors mappings --provider <p> List the org's field mappings for a provider
234
+ connectors bind --collection <uuid> --provider <p> [--mapping <uuid>]
235
+ Wire a collection to a CMS + field mapping
211
236
 
212
237
  Strategy & onboarding:
213
238
  strategy company get | set [--name] [--domain] [--manifesto <text>|--manifesto-file <path>]
@@ -295,6 +320,34 @@ Collection onboarding (your OWN blog, on your org's domain — no provisioning):
295
320
  --watch polls to completion, printing each
296
321
  review note as it lands.
297
322
 
323
+ Domain onboarding (ONE SHOT: a bare domain in, a fully-configured organization out):
324
+ ${bin === "phantom" ? "onboard" : "phantom-onboard"} domain [--about "the direction"] [--sitemap <url>] [--cms <provider>]
325
+ [--name "<main collection name>"] [--posts N] [--no-phantom] [--phantom-name X]
326
+ [--site-domain host] [--domain-mode subdomain|buy_now|connect_later]
327
+ [--seers suggest|skip] [--no-visuals] [--no-enhance] [--test]
328
+ [--readiness-minutes N] [--watch]
329
+ On the org behind the key (its registered
330
+ domain): onboard the main blog (the
331
+ collection recipe), infer org visuals from
332
+ the site, configure the main blog's Enhance
333
+ flow, provision + configure + seed a phantom
334
+ (default 3 posts, written and published),
335
+ and create Seers in suggest mode when they
336
+ fit — then wait (default 2h) for the seeds
337
+ to publish and report what landed.
338
+ ${bin === "phantom" ? "onboard" : "phantom-onboard"} domain --attach <job-id> [--watch]
339
+ Watch / read a job started elsewhere.
340
+ ${bin === "phantom" ? "onboard" : "phantom-onboard"} domain --status Latest domain onboarding on this org.
341
+
342
+ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org exists):
343
+ admin create-org --domain <bare-domain> [--name "Acme"] [--test] [--no-key]
344
+ [--key-name N] [--capabilities a,b] [--onboard] [+ the onboard domain flags]
345
+ The key exchange: create the client org from
346
+ its domain (one org per domain), mint an
347
+ ORG-scoped API key and print it ONCE. With
348
+ --onboard the domain onboarding starts in the
349
+ same call; watch it with the printed key.
350
+
298
351
  Seers (event-driven signals -> drafts):
299
352
  seers list List seers with recent activity
300
353
  seers get <seer-id> One seer's full config + poll state
@@ -414,6 +467,8 @@ const CLIENT_COMMANDS = {
414
467
  templates: cmdTemplates,
415
468
  shredder: cmdShredder,
416
469
  landings: cmdLandings,
470
+ // Platform-scoped (staff key) — the only commands that act BEFORE an org exists.
471
+ admin: cmdAdmin,
417
472
  };
418
473
 
419
474
  // LETTERSTORY_POLL_INTERVAL_MS / LETTERSTORY_MAX_POLLS let an operator (or an
@@ -0,0 +1,146 @@
1
+ // `admin` — the PLATFORM-scoped commands, for staff. Everything else in this CLI is
2
+ // org-scoped and rides the MCP endpoint with an org API key; these ride a STAFF API key
3
+ // (`x-staff-api-key`, minted at Settings → Staff keys) against the superadmin REST
4
+ // routes, because they exist precisely to act BEFORE an org exists.
5
+ //
6
+ // admin create-org --domain acme.com [--name "Acme"] [--test] [--no-key] [--key-name N]
7
+ // [--capabilities a,b] [--onboard] [--about "…"] [--sitemap <url>]
8
+ // [--cms <provider>] [--posts N] [--seers suggest|skip] [--no-visuals]
9
+ // [--no-enhance] [--readiness-minutes N]
10
+ //
11
+ // The KEY EXCHANGE: create the client org from its domain (one org per domain; an existing
12
+ // owner is adopted), mint an ORG-scoped API key for it and print the plaintext ONCE. With
13
+ // --onboard, the one-shot domain onboarding (main blog + org visuals + enhance + phantom +
14
+ // seers) starts in the same call; watch it afterwards with
15
+ // LETTERSTORY_API_KEY=<printed key> letterstory phantom-onboard domain --watch --attach <job-id>
16
+ //
17
+ // The staff key comes from --staff-key or LETTERSTORY_STAFF_API_KEY; the base URL from the
18
+ // usual --url / LETTERSTORY_API_URL / config resolution.
19
+
20
+ import { CliError } from "../client.mjs";
21
+ import { flagStr, flagNum, flagBool, flagList, printResult, compact, ok } from "./shared.mjs";
22
+
23
+ const SEER_MODES = ["suggest", "skip"];
24
+ const CMS_PROVIDERS = ["webflow", "framer", "sanity", "contentful"];
25
+
26
+ function staffKeyFrom(flags) {
27
+ const key = flagStr(flags["staff-key"]) || process.env.LETTERSTORY_STAFF_API_KEY || null;
28
+ if (!key) {
29
+ throw new CliError(
30
+ "admin commands need a STAFF API key: pass --staff-key <key> or set LETTERSTORY_STAFF_API_KEY " +
31
+ "(mint one at Settings → Staff keys with the org:create capability)."
32
+ );
33
+ }
34
+ return key;
35
+ }
36
+
37
+ /** POST a superadmin route with the staff key; surfaces the server's error text on non-2xx. */
38
+ async function staffPost(ctx, path, body) {
39
+ const { client, flags } = ctx;
40
+ const url = `${client.url}${path}`;
41
+ const res = await client.fetch(url, {
42
+ method: "POST",
43
+ headers: { "content-type": "application/json", "x-staff-api-key": staffKeyFrom(flags) },
44
+ body: JSON.stringify(body),
45
+ });
46
+ let payload = null;
47
+ try {
48
+ payload = await res.json();
49
+ } catch {
50
+ payload = null;
51
+ }
52
+ if (!res.ok) {
53
+ const message = payload?.error ?? `${res.status} ${res.statusText}`;
54
+ throw new CliError(`${path} failed: ${message}`);
55
+ }
56
+ return payload ?? {};
57
+ }
58
+
59
+ function onboardOptionsFromFlags(flags) {
60
+ const seers = flagStr(flags.seers);
61
+ if (seers !== undefined && !SEER_MODES.includes(seers)) {
62
+ throw new CliError(`--seers must be one of: ${SEER_MODES.join(", ")}`);
63
+ }
64
+ const cms = flagStr(flags.cms);
65
+ if (cms !== undefined && !CMS_PROVIDERS.includes(cms)) {
66
+ throw new CliError(`--cms must be one of: ${CMS_PROVIDERS.join(", ")}`);
67
+ }
68
+ const posts = flagNum(flags.posts);
69
+ const options = compact({
70
+ phantom:
71
+ posts !== undefined || flagBool(flags["no-phantom"])
72
+ ? compact({ enabled: flagBool(flags["no-phantom"]) ? false : undefined, posts_to_seed: posts })
73
+ : undefined,
74
+ seers,
75
+ visuals: flagBool(flags["no-visuals"]) ? false : undefined,
76
+ enhance: flagBool(flags["no-enhance"]) ? false : undefined,
77
+ readiness_minutes: flagNum(flags["readiness-minutes"]),
78
+ });
79
+ return compact({
80
+ direction: flagStr(flags.about) ?? flagStr(flags.direction),
81
+ sitemap_url: flagStr(flags.sitemap),
82
+ cms_provider: cms,
83
+ options: Object.keys(options).length ? options : undefined,
84
+ });
85
+ }
86
+
87
+ function formatCreateOrg(r) {
88
+ const lines = [
89
+ `${r.org_created ? "Created" : "Reused"} organization "${r.org_name}" (${r.domain})`,
90
+ ` org_id: ${r.org_id}`,
91
+ ];
92
+ if (r.api_key) {
93
+ lines.push(
94
+ ` api key "${r.api_key.name}" (${(r.api_key.capabilities ?? []).length} capabilities):`,
95
+ ` ${r.api_key.plain_key}`,
96
+ ` ${r.api_key.warning ?? "Save this key now — it is not shown again."}`
97
+ );
98
+ }
99
+ if (r.job_id) {
100
+ lines.push(
101
+ ` domain onboarding started: job ${r.job_id}`,
102
+ ` watch it: LETTERSTORY_API_KEY=<the key above> letterstory phantom-onboard domain --attach ${r.job_id} --watch`
103
+ );
104
+ }
105
+ if (r.error) lines.push(` error: ${r.error}`);
106
+ return lines.join("\n");
107
+ }
108
+
109
+ async function cmdAdminCreateOrg(ctx) {
110
+ const { flags, io } = ctx;
111
+ const domain = flagStr(flags.domain);
112
+ if (!domain) throw new CliError("admin create-org requires --domain <bare-domain>.");
113
+
114
+ // --capabilities a,b or repeated --capabilities a --capabilities b.
115
+ const capabilities = flagList(flags.capabilities)
116
+ .flatMap((v) => v.split(","))
117
+ .map((v) => v.trim())
118
+ .filter(Boolean);
119
+ const key = flagBool(flags["no-key"])
120
+ ? false
121
+ : capabilities?.length || flagStr(flags["key-name"])
122
+ ? compact({
123
+ name: flagStr(flags["key-name"]),
124
+ capabilities: capabilities?.length ? capabilities : undefined,
125
+ })
126
+ : true;
127
+
128
+ const body = compact({
129
+ domain,
130
+ name: flagStr(flags.name),
131
+ is_test: flagBool(flags.test) ? true : undefined,
132
+ key,
133
+ onboard: flagBool(flags.onboard) ? onboardOptionsFromFlags(flags) : undefined,
134
+ });
135
+
136
+ ok(ctx, `Provisioning ${domain}…`);
137
+ const result = await staffPost(ctx, "/api/superadmin/orgs", body);
138
+ printResult(io, flags, result, formatCreateOrg);
139
+ return 0;
140
+ }
141
+
142
+ export async function cmdAdmin(ctx) {
143
+ const sub = ctx.positionals[0];
144
+ if (sub === "create-org") return cmdAdminCreateOrg({ ...ctx, positionals: ctx.positionals.slice(1) });
145
+ throw new CliError("Usage: admin create-org --domain <bare-domain> [--onboard] … (see --help)");
146
+ }
@@ -20,8 +20,20 @@ export async function cmdConnectors(ctx) {
20
20
  case "pull":
21
21
  case "import":
22
22
  return connectorsPull(rest);
23
+ case "connect":
24
+ return connectorsConnect(rest);
25
+ case "disconnect":
26
+ return connectorsDisconnect(rest);
27
+ case "connections":
28
+ return connectorsConnections(rest);
29
+ case "mappings":
30
+ return connectorsMappings(rest);
31
+ case "bind":
32
+ return connectorsBind(rest);
23
33
  default:
24
- throw new CliError(`Unknown connectors subcommand: ${sub ?? "(none)"}. Try: list, publish, status, pull`);
34
+ throw new CliError(
35
+ `Unknown connectors subcommand: ${sub ?? "(none)"}. Try: list, publish, status, pull, connect, disconnect, connections, mappings, bind`
36
+ );
25
37
  }
26
38
  }
27
39
 
@@ -81,3 +93,67 @@ async function connectorsPull(ctx) {
81
93
  printResult(io, flags, result);
82
94
  return 0;
83
95
  }
96
+
97
+ async function connectorsConnect(ctx) {
98
+ const { client, flags, io } = ctx;
99
+ const provider = requireFlag(flags, "provider");
100
+ // api_token is required for every provider; the rest are provider-specific.
101
+ const args = compact({
102
+ provider,
103
+ api_token: requireFlag(flags, "token"),
104
+ project_url: flagStr(flags["project-url"]),
105
+ auto_deploy: flags["no-auto-deploy"] ? false : flagBool(flags["auto-deploy"]) || undefined,
106
+ project_id: flagStr(flags["project-id"]),
107
+ dataset: flagStr(flags.dataset),
108
+ project_name: flagStr(flags["project-name"]),
109
+ workspace_name: flagStr(flags.workspace),
110
+ space_id: flagStr(flags["space-id"]),
111
+ environment_id: flagStr(flags.environment),
112
+ space_name: flagStr(flags["space-name"]),
113
+ default_locale: flagStr(flags.locale),
114
+ });
115
+ const result = await client.callTool("connect_cms", args);
116
+ ok(ctx, `Connected ${provider}${result?.label ? ` (${result.label})` : ""}.`);
117
+ printResult(io, flags, result);
118
+ return 0;
119
+ }
120
+
121
+ async function connectorsDisconnect(ctx) {
122
+ const { client, flags, io } = ctx;
123
+ const provider = requireFlag(flags, "provider");
124
+ const result = await client.callTool("disconnect_cms", { provider });
125
+ ok(ctx, `Disconnected ${provider}.`);
126
+ printResult(io, flags, result);
127
+ return 0;
128
+ }
129
+
130
+ async function connectorsConnections(ctx) {
131
+ const { client, flags, io } = ctx;
132
+ const result = await client.callTool("list_cms_connections", {});
133
+ printResult(io, flags, result);
134
+ return 0;
135
+ }
136
+
137
+ async function connectorsMappings(ctx) {
138
+ const { client, flags, io } = ctx;
139
+ const provider = requireFlag(flags, "provider");
140
+ const result = await client.callTool("list_cms_mappings", { provider });
141
+ printResult(io, flags, result);
142
+ return 0;
143
+ }
144
+
145
+ async function connectorsBind(ctx) {
146
+ const { client, flags, io } = ctx;
147
+ const args = compact({
148
+ collection_id: requireFlag(flags, "collection"),
149
+ provider: requireFlag(flags, "provider"),
150
+ mapping_id: flagStr(flags.mapping),
151
+ });
152
+ const result = await client.callTool("bind_collection_cms", args);
153
+ ok(
154
+ ctx,
155
+ `Bound collection ${result?.collection_id ?? ""} to ${result?.provider ?? ""} (mapping ${result?.mapping_id ?? ""}).`
156
+ );
157
+ printResult(io, flags, result);
158
+ return 0;
159
+ }
@@ -18,6 +18,14 @@
18
18
  // posts are written by background jobs (20-40min each); they're reported as job ids and
19
19
  // this command does not block on them — poll with `phantom-job status <job-id>`.
20
20
 
21
+ // `onboard domain` is the ONE-SHOT parent: a bare company domain in (the org's registered
22
+ // domain, from the API key), a fully-configured organization out — the main blog onboarded
23
+ // (the `collection` recipe), org visuals inferred from the site, the main blog's Enhance
24
+ // flow configured, a phantom provisioned + configured + seeded, Seers in suggest mode —
25
+ // driven by `onboard_domain` + `get_domain_onboard_status`. `--watch` polls to completion;
26
+ // `--attach <job-id>` watches a job started elsewhere (e.g. `admin create-org --onboard`).
27
+ // To create the ORG itself headlessly, use `admin create-org` with a staff key first.
28
+
21
29
  // `onboard collection` is the CLIENT-blog sibling: the same recipe minus provisioning,
22
30
  // driven by `onboard_collection` + `get_collection_onboard_status`. The org (and its
23
31
  // domain) comes from the API key — the job reads the org's registered domain, imports
@@ -216,12 +224,155 @@ async function cmdOnboardSyncSitemap(ctx) {
216
224
  return 0;
217
225
  }
218
226
 
227
+ const SEER_MODES = ["suggest", "skip"];
228
+ const CMS_PROVIDERS = ["webflow", "framer", "sanity", "contentful"];
229
+
230
+ // Final readback of a domain-onboarding job — the counts the engine PERSISTED.
231
+ function formatDomainStatus(s) {
232
+ const r = s.result ?? {};
233
+ const lines = [
234
+ `Domain onboarding ${s.status} at stage "${s.stage}" (${s.domain})`,
235
+ ` org: ${s.org_id}`,
236
+ ` main blog: ${s.collection_id ?? "(pending)"}${s.collection_job_id ? ` (job ${s.collection_job_id})` : ""}`,
237
+ ` phantom job: ${s.phantom_job_id ?? "(none)"}`,
238
+ ];
239
+ if (s.error) lines.push(` error: ${s.error}`);
240
+ if (r.collection) {
241
+ lines.push(
242
+ ` posts imported: ${r.collection.posts_imported ?? 0} · content areas ${r.collection.coverage_areas ?? 0} · pillars ${r.collection.pillars ?? 0} · headlines ${r.collection.headlines_suggested ?? 0}`
243
+ );
244
+ }
245
+ if (r.visuals) {
246
+ lines.push(
247
+ ` org visuals: ${r.visuals.skipped_reason ? `skipped (${r.visuals.skipped_reason})` : `${r.visuals.created} created, ${r.visuals.reused} reused; bound ${(r.visuals.bound ?? []).join("/") || "nothing"}`}`
248
+ );
249
+ }
250
+ if (r.enhance)
251
+ lines.push(` enhance flow: ${r.enhance.action}${r.enhance.flow_id ? ` (${r.enhance.flow_id})` : ""}`);
252
+ if (r.phantom) {
253
+ lines.push(
254
+ ` phantom: ${r.phantom.site_url ?? r.phantom.deployment_id ?? "(no site)"} — ${r.phantom.status}, seeds ${r.phantom.seeds_published}/${r.phantom.seeds_total} published`
255
+ );
256
+ }
257
+ if (r.seers) {
258
+ lines.push(
259
+ ` seers: ${r.seers.skipped_reason ? `skipped (${r.seers.skipped_reason})` : `${r.seers.created} created (suggest mode), ${r.seers.reused} reused`}`
260
+ );
261
+ }
262
+ if (r.probes) lines.push(` GEO probes: ${r.probes.runs} run(s), ${r.probes.responses} answers`);
263
+ for (const w of r.warnings ?? []) lines.push(` warning: ${w}`);
264
+ return lines.join("\n");
265
+ }
266
+
267
+ async function watchDomainJob(ctx, jobId) {
268
+ const { client, flags } = ctx;
269
+ let printed = 0;
270
+ let lastStage = null;
271
+ const final = await pollUntil(
272
+ ctx.io,
273
+ async () => {
274
+ const s = await client.callTool("get_domain_onboard_status", { job_id: jobId });
275
+ if (s.stage !== lastStage) {
276
+ ok(ctx, ` … ${s.stage}`);
277
+ lastStage = s.stage;
278
+ }
279
+ const review = Array.isArray(s.review) ? s.review : [];
280
+ for (; printed < review.length; printed++) ok(ctx, ` ${review[printed]}`);
281
+ if (s.status === "failed") {
282
+ throw new CliError(
283
+ `Domain onboarding failed at stage "${s.stage}": ${s.error ?? "unknown error"} (the main blog job, if it ran, keeps what it imported).`
284
+ );
285
+ }
286
+ return { done: s.status !== "running", value: s };
287
+ },
288
+ // The readiness stage waits for seed posts (default up to 2h); the watch window
289
+ // covers the default and a little slack.
290
+ { label: "domain onboarding to finish", timeoutMs: 150 * 60 * 1000, intervalMs: 10000 }
291
+ );
292
+ printResult(ctx.io, flags, final, formatDomainStatus);
293
+ return 0;
294
+ }
295
+
296
+ // `onboard domain` — the one-shot parent. The org and its domain come from the API key.
297
+ async function cmdOnboardDomain(ctx) {
298
+ const { client, flags } = ctx;
299
+
300
+ // Attach to a job started elsewhere (admin create-org --onboard prints one).
301
+ const attach = flagStr(flags.attach);
302
+ if (attach) {
303
+ if (!flagBool(flags.watch)) {
304
+ const s = await client.callTool("get_domain_onboard_status", { job_id: attach });
305
+ printResult(ctx.io, flags, s, formatDomainStatus);
306
+ return 0;
307
+ }
308
+ return watchDomainJob(ctx, attach);
309
+ }
310
+ if (flagBool(flags.status)) {
311
+ const s = await client.callTool("get_domain_onboard_status", {});
312
+ printResult(ctx.io, flags, s, formatDomainStatus);
313
+ return 0;
314
+ }
315
+
316
+ const seers = flagStr(flags.seers);
317
+ if (seers !== undefined && !SEER_MODES.includes(seers)) {
318
+ throw new CliError(`--seers must be one of: ${SEER_MODES.join(", ")}`);
319
+ }
320
+ const cms = flagStr(flags.cms);
321
+ if (cms !== undefined && !CMS_PROVIDERS.includes(cms)) {
322
+ throw new CliError(`--cms must be one of: ${CMS_PROVIDERS.join(", ")}`);
323
+ }
324
+ const posts = flagNum(flags.posts);
325
+ const phantom = compact({
326
+ enabled: flagBool(flags["no-phantom"]) ? false : undefined,
327
+ posts_to_seed: posts,
328
+ name: flagStr(flags["phantom-name"]),
329
+ site_domain: flagStr(flags["site-domain"]),
330
+ domain_mode: flagStr(flags["domain-mode"]),
331
+ });
332
+ const args = compact({
333
+ direction: flagStr(flags.about) ?? flagStr(flags.direction),
334
+ sitemap_url: flagStr(flags.sitemap),
335
+ cms_provider: cms,
336
+ collection_name: flagStr(flags.name),
337
+ phantom: Object.keys(phantom).length ? phantom : undefined,
338
+ seers,
339
+ visuals: flagBool(flags["no-visuals"]) ? false : undefined,
340
+ enhance: flagBool(flags["no-enhance"]) ? false : undefined,
341
+ is_test: flagBool(flags.test) ? true : undefined,
342
+ readiness_minutes: flagNum(flags["readiness-minutes"]),
343
+ });
344
+
345
+ ok(ctx, "Starting domain onboarding…");
346
+ const started = await client.callTool("onboard_domain", args);
347
+ if (!started.job_id) throw new CliError("onboard_domain did not return a job id.");
348
+ ok(ctx, `Job ${started.job_id} running for ${started.domain}.`);
349
+
350
+ if (!flagBool(flags.watch)) {
351
+ printResult(ctx.io, flags, started, (s) =>
352
+ [
353
+ `Domain onboarding started (stage "${s.stage}").`,
354
+ ` job: ${s.job_id}`,
355
+ ` org: ${s.org_id}`,
356
+ ` domain: ${s.domain}`,
357
+ `Watch it with: ${ctx.bin} ${ctx.positionals.length ? "phantom-onboard" : "onboard"} domain --attach ${s.job_id} --watch, or poll:`,
358
+ ` ${ctx.bin} call get_domain_onboard_status --args '{"job_id":"${s.job_id}"}'`,
359
+ ].join("\n")
360
+ );
361
+ return 0;
362
+ }
363
+ return watchDomainJob(ctx, started.job_id);
364
+ }
365
+
219
366
  export async function cmdOnboard(ctx) {
220
367
  const { client, flags, io } = ctx;
221
368
  // `onboard collection …` — the client-blog variant (no <domain>; org from the key).
222
369
  if (ctx.positionals[0] === "collection") {
223
370
  return cmdOnboardCollection({ ...ctx, positionals: ctx.positionals.slice(1) });
224
371
  }
372
+ // `onboard domain …` — the one-shot parent (org + domain from the key).
373
+ if (ctx.positionals[0] === "domain") {
374
+ return cmdOnboardDomain({ ...ctx, positionals: ctx.positionals.slice(1) });
375
+ }
225
376
  // `onboard sync-sitemap --collection <uuid>` — refresh an already-onboarded blog.
226
377
  if (ctx.positionals[0] === "sync-sitemap") {
227
378
  return cmdOnboardSyncSitemap({ ...ctx, positionals: ctx.positionals.slice(1) });
package/lib/commands.mjs CHANGED
@@ -25,3 +25,4 @@ export * from "./commands/onboard.mjs";
25
25
  export * from "./commands/seers.mjs";
26
26
  export * from "./commands/shredder.mjs";
27
27
  export * from "./commands/landings.mjs";
28
+ export * from "./commands/admin.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",