@letterstory/cli 0.13.0 → 0.15.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,38 @@ 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: the key exchange (create the org first) needs a STAFF key with org:create —
251
+ # mint your OWN, don't share one. --watch makes the whole recipe ONE command: the
252
+ # freshly-minted org key is used only in this process's memory to poll to completion.
253
+ LETTERSTORY_STAFF_API_KEY=<your staff key with org:create> \
254
+ letterstory admin create-org --domain acme.com --onboard --watch --about "payroll for platform teams"
255
+
256
+ # Without --watch, it prints the org id, a fresh ORG-scoped API key (once), and the job
257
+ # id — watch it yourself with:
258
+ # LETTERSTORY_API_KEY=<the printed key> letterstory phantom-onboard domain --attach <job-id> --watch
259
+ ```
260
+
261
+ Everything the parent job does is also its own tool for existing orgs:
262
+ `ensure_collection_enhance_flow`, `provision_org_visuals`, `propose_seers` (`--args '{"apply":true}'`).
263
+
232
264
  ## Content series
233
265
 
234
266
  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.13.0";
44
+ export const VERSION = "0.15.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.
@@ -71,6 +72,16 @@ const BOOLEAN_FLAGS = new Set([
71
72
  "stock-only",
72
73
  "gate",
73
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",
74
85
  ]);
75
86
 
76
87
  // Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
@@ -309,6 +320,39 @@ Collection onboarding (your OWN blog, on your org's domain — no provisioning):
309
320
  --watch polls to completion, printing each
310
321
  review note as it lands.
311
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] [--watch] [+ 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; add --watch to poll it to
350
+ completion in this same command — the org key
351
+ is used only in memory for the poll, never
352
+ written to disk or printed twice — a bare
353
+ domain in, a fully-configured org out, one
354
+ command.
355
+
312
356
  Seers (event-driven signals -> drafts):
313
357
  seers list List seers with recent activity
314
358
  seers get <seer-id> One seer's full config + poll state
@@ -428,6 +472,8 @@ const CLIENT_COMMANDS = {
428
472
  templates: cmdTemplates,
429
473
  shredder: cmdShredder,
430
474
  landings: cmdLandings,
475
+ // Platform-scoped (staff key) — the only commands that act BEFORE an org exists.
476
+ admin: cmdAdmin,
431
477
  };
432
478
 
433
479
  // LETTERSTORY_POLL_INTERVAL_MS / LETTERSTORY_MAX_POLLS let an operator (or an
@@ -0,0 +1,184 @@
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] [--watch]
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. --onboard --watch makes the whole recipe ONE command: the
15
+ // freshly-minted org key is used ONLY in this process's memory (never written to disk, never
16
+ // re-printed) to poll the job to completion, reusing the exact same loop
17
+ // `phantom-onboard domain --watch` uses. Without --watch, watch it afterwards with
18
+ // LETTERSTORY_API_KEY=<printed key> letterstory phantom-onboard domain --watch --attach <job-id>
19
+ //
20
+ // The staff key comes from --staff-key or LETTERSTORY_STAFF_API_KEY; the base URL from the
21
+ // usual --url / LETTERSTORY_API_URL / config resolution.
22
+
23
+ import { CliError, LetterstoryClient } from "../client.mjs";
24
+ import { flagStr, flagNum, flagBool, flagList, printResult, compact, ok } from "./shared.mjs";
25
+ import { watchDomainJob } from "./onboard.mjs";
26
+
27
+ const SEER_MODES = ["suggest", "skip"];
28
+ const CMS_PROVIDERS = ["webflow", "framer", "sanity", "contentful"];
29
+
30
+ function staffKeyFrom(flags) {
31
+ const key = flagStr(flags["staff-key"]) || process.env.LETTERSTORY_STAFF_API_KEY || null;
32
+ if (!key) {
33
+ throw new CliError(
34
+ "admin commands need a STAFF API key: pass --staff-key <key> or set LETTERSTORY_STAFF_API_KEY " +
35
+ "(mint one at Settings → Staff keys with the org:create capability)."
36
+ );
37
+ }
38
+ return key;
39
+ }
40
+
41
+ /** POST a superadmin route with the staff key; surfaces the server's error text on non-2xx. */
42
+ async function staffPost(ctx, path, body) {
43
+ const { client, flags } = ctx;
44
+ const url = `${client.url}${path}`;
45
+ const res = await client.fetch(url, {
46
+ method: "POST",
47
+ headers: { "content-type": "application/json", "x-staff-api-key": staffKeyFrom(flags) },
48
+ body: JSON.stringify(body),
49
+ });
50
+ let payload = null;
51
+ try {
52
+ payload = await res.json();
53
+ } catch {
54
+ payload = null;
55
+ }
56
+ if (!res.ok) {
57
+ const message = payload?.error ?? `${res.status} ${res.statusText}`;
58
+ throw new CliError(`${path} failed: ${message}`);
59
+ }
60
+ return payload ?? {};
61
+ }
62
+
63
+ function onboardOptionsFromFlags(flags) {
64
+ const seers = flagStr(flags.seers);
65
+ if (seers !== undefined && !SEER_MODES.includes(seers)) {
66
+ throw new CliError(`--seers must be one of: ${SEER_MODES.join(", ")}`);
67
+ }
68
+ const cms = flagStr(flags.cms);
69
+ if (cms !== undefined && !CMS_PROVIDERS.includes(cms)) {
70
+ throw new CliError(`--cms must be one of: ${CMS_PROVIDERS.join(", ")}`);
71
+ }
72
+ const posts = flagNum(flags.posts);
73
+ const options = compact({
74
+ phantom:
75
+ posts !== undefined || flagBool(flags["no-phantom"])
76
+ ? compact({ enabled: flagBool(flags["no-phantom"]) ? false : undefined, posts_to_seed: posts })
77
+ : undefined,
78
+ seers,
79
+ visuals: flagBool(flags["no-visuals"]) ? false : undefined,
80
+ enhance: flagBool(flags["no-enhance"]) ? false : undefined,
81
+ readiness_minutes: flagNum(flags["readiness-minutes"]),
82
+ });
83
+ return compact({
84
+ direction: flagStr(flags.about) ?? flagStr(flags.direction),
85
+ sitemap_url: flagStr(flags.sitemap),
86
+ cms_provider: cms,
87
+ options: Object.keys(options).length ? options : undefined,
88
+ });
89
+ }
90
+
91
+ function formatCreateOrg(r) {
92
+ const lines = [
93
+ `${r.org_created ? "Created" : "Reused"} organization "${r.org_name}" (${r.domain})`,
94
+ ` org_id: ${r.org_id}`,
95
+ ];
96
+ if (r.api_key) {
97
+ lines.push(
98
+ ` api key "${r.api_key.name}" (${(r.api_key.capabilities ?? []).length} capabilities):`,
99
+ ` ${r.api_key.plain_key}`,
100
+ ` ${r.api_key.warning ?? "Save this key now — it is not shown again."}`
101
+ );
102
+ }
103
+ if (r.job_id) {
104
+ lines.push(
105
+ ` domain onboarding started: job ${r.job_id}`,
106
+ ` watch it: LETTERSTORY_API_KEY=<the key above> letterstory phantom-onboard domain --attach ${r.job_id} --watch`
107
+ );
108
+ }
109
+ if (r.error) lines.push(` error: ${r.error}`);
110
+ return lines.join("\n");
111
+ }
112
+
113
+ async function cmdAdminCreateOrg(ctx) {
114
+ const { flags, io, client } = ctx;
115
+ const domain = flagStr(flags.domain);
116
+ if (!domain) throw new CliError("admin create-org requires --domain <bare-domain>.");
117
+
118
+ const watch = flagBool(flags.watch);
119
+ if (watch && !flagBool(flags.onboard)) {
120
+ throw new CliError("--watch needs --onboard (nothing to watch without a job).");
121
+ }
122
+ if (watch && flagBool(flags["no-key"])) {
123
+ throw new CliError("--watch needs an org key to poll with — drop --no-key.");
124
+ }
125
+
126
+ // --capabilities a,b or repeated --capabilities a --capabilities b.
127
+ const capabilities = flagList(flags.capabilities)
128
+ .flatMap((v) => v.split(","))
129
+ .map((v) => v.trim())
130
+ .filter(Boolean);
131
+ const key = flagBool(flags["no-key"])
132
+ ? false
133
+ : capabilities?.length || flagStr(flags["key-name"])
134
+ ? compact({
135
+ name: flagStr(flags["key-name"]),
136
+ capabilities: capabilities?.length ? capabilities : undefined,
137
+ })
138
+ : true;
139
+
140
+ const body = compact({
141
+ domain,
142
+ name: flagStr(flags.name),
143
+ is_test: flagBool(flags.test) ? true : undefined,
144
+ key,
145
+ onboard: flagBool(flags.onboard) ? onboardOptionsFromFlags(flags) : undefined,
146
+ });
147
+
148
+ ok(ctx, `Provisioning ${domain}…`);
149
+ const result = await staffPost(ctx, "/api/superadmin/orgs", body);
150
+
151
+ if (!watch) {
152
+ printResult(io, flags, result, formatCreateOrg);
153
+ return 0;
154
+ }
155
+
156
+ // --watch: everything spins up from this one command. The org key exists ONLY in this
157
+ // process's memory for the rest of the run — never written to disk, never printed again
158
+ // after the summary above — and is used to build a second, throwaway MCP client that
159
+ // polls the SAME loop `phantom-onboard domain --watch` uses. `ok(...)` here (not
160
+ // printResult) so the key/warning above are still shown before the poll starts.
161
+ ok(ctx, formatCreateOrg(result));
162
+ if (!result.job_id) {
163
+ ok(ctx, "No job to watch (the org call didn't start one — see the error above, if any).");
164
+ return 0;
165
+ }
166
+ if (!result.api_key?.plain_key) {
167
+ throw new CliError("No org key came back to watch with (did --no-key sneak in?).");
168
+ }
169
+ const orgClient = new LetterstoryClient({
170
+ url: client.url,
171
+ key: result.api_key.plain_key,
172
+ fetchImpl: client.fetch,
173
+ verbose: client.verbose,
174
+ verboseLog: client.verboseLog,
175
+ bin: ctx.bin,
176
+ });
177
+ return watchDomainJob({ ...ctx, client: orgClient }, result.job_id);
178
+ }
179
+
180
+ export async function cmdAdmin(ctx) {
181
+ const sub = ctx.positionals[0];
182
+ if (sub === "create-org") return cmdAdminCreateOrg({ ...ctx, positionals: ctx.positionals.slice(1) });
183
+ throw new CliError("Usage: admin create-org --domain <bare-domain> [--onboard] … (see --help)");
184
+ }
@@ -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,157 @@ 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
+ export 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
+ // Exported so `admin create-org --onboard --watch` can drive the SAME poll loop against a
268
+ // freshly-minted org key, instead of duplicating it — see admin.mjs.
269
+ export async function watchDomainJob(ctx, jobId) {
270
+ const { client, flags } = ctx;
271
+ let printed = 0;
272
+ let lastStage = null;
273
+ const final = await pollUntil(
274
+ ctx.io,
275
+ async () => {
276
+ const s = await client.callTool("get_domain_onboard_status", { job_id: jobId });
277
+ if (s.stage !== lastStage) {
278
+ ok(ctx, ` … ${s.stage}`);
279
+ lastStage = s.stage;
280
+ }
281
+ const review = Array.isArray(s.review) ? s.review : [];
282
+ for (; printed < review.length; printed++) ok(ctx, ` ${review[printed]}`);
283
+ if (s.status === "failed") {
284
+ throw new CliError(
285
+ `Domain onboarding failed at stage "${s.stage}": ${s.error ?? "unknown error"} (the main blog job, if it ran, keeps what it imported).`
286
+ );
287
+ }
288
+ return { done: s.status !== "running", value: s };
289
+ },
290
+ // The readiness stage waits for seed posts (default up to 2h); the watch window
291
+ // covers the default and a little slack.
292
+ { label: "domain onboarding to finish", timeoutMs: 150 * 60 * 1000, intervalMs: 10000 }
293
+ );
294
+ printResult(ctx.io, flags, final, formatDomainStatus);
295
+ return 0;
296
+ }
297
+
298
+ // `onboard domain` — the one-shot parent. The org and its domain come from the API key.
299
+ async function cmdOnboardDomain(ctx) {
300
+ const { client, flags } = ctx;
301
+
302
+ // Attach to a job started elsewhere (admin create-org --onboard prints one).
303
+ const attach = flagStr(flags.attach);
304
+ if (attach) {
305
+ if (!flagBool(flags.watch)) {
306
+ const s = await client.callTool("get_domain_onboard_status", { job_id: attach });
307
+ printResult(ctx.io, flags, s, formatDomainStatus);
308
+ return 0;
309
+ }
310
+ return watchDomainJob(ctx, attach);
311
+ }
312
+ if (flagBool(flags.status)) {
313
+ const s = await client.callTool("get_domain_onboard_status", {});
314
+ printResult(ctx.io, flags, s, formatDomainStatus);
315
+ return 0;
316
+ }
317
+
318
+ const seers = flagStr(flags.seers);
319
+ if (seers !== undefined && !SEER_MODES.includes(seers)) {
320
+ throw new CliError(`--seers must be one of: ${SEER_MODES.join(", ")}`);
321
+ }
322
+ const cms = flagStr(flags.cms);
323
+ if (cms !== undefined && !CMS_PROVIDERS.includes(cms)) {
324
+ throw new CliError(`--cms must be one of: ${CMS_PROVIDERS.join(", ")}`);
325
+ }
326
+ const posts = flagNum(flags.posts);
327
+ const phantom = compact({
328
+ enabled: flagBool(flags["no-phantom"]) ? false : undefined,
329
+ posts_to_seed: posts,
330
+ name: flagStr(flags["phantom-name"]),
331
+ site_domain: flagStr(flags["site-domain"]),
332
+ domain_mode: flagStr(flags["domain-mode"]),
333
+ });
334
+ const args = compact({
335
+ direction: flagStr(flags.about) ?? flagStr(flags.direction),
336
+ sitemap_url: flagStr(flags.sitemap),
337
+ cms_provider: cms,
338
+ collection_name: flagStr(flags.name),
339
+ phantom: Object.keys(phantom).length ? phantom : undefined,
340
+ seers,
341
+ visuals: flagBool(flags["no-visuals"]) ? false : undefined,
342
+ enhance: flagBool(flags["no-enhance"]) ? false : undefined,
343
+ is_test: flagBool(flags.test) ? true : undefined,
344
+ readiness_minutes: flagNum(flags["readiness-minutes"]),
345
+ });
346
+
347
+ ok(ctx, "Starting domain onboarding…");
348
+ const started = await client.callTool("onboard_domain", args);
349
+ if (!started.job_id) throw new CliError("onboard_domain did not return a job id.");
350
+ ok(ctx, `Job ${started.job_id} running for ${started.domain}.`);
351
+
352
+ if (!flagBool(flags.watch)) {
353
+ printResult(ctx.io, flags, started, (s) =>
354
+ [
355
+ `Domain onboarding started (stage "${s.stage}").`,
356
+ ` job: ${s.job_id}`,
357
+ ` org: ${s.org_id}`,
358
+ ` domain: ${s.domain}`,
359
+ `Watch it with: ${ctx.bin} ${ctx.positionals.length ? "phantom-onboard" : "onboard"} domain --attach ${s.job_id} --watch, or poll:`,
360
+ ` ${ctx.bin} call get_domain_onboard_status --args '{"job_id":"${s.job_id}"}'`,
361
+ ].join("\n")
362
+ );
363
+ return 0;
364
+ }
365
+ return watchDomainJob(ctx, started.job_id);
366
+ }
367
+
219
368
  export async function cmdOnboard(ctx) {
220
369
  const { client, flags, io } = ctx;
221
370
  // `onboard collection …` — the client-blog variant (no <domain>; org from the key).
222
371
  if (ctx.positionals[0] === "collection") {
223
372
  return cmdOnboardCollection({ ...ctx, positionals: ctx.positionals.slice(1) });
224
373
  }
374
+ // `onboard domain …` — the one-shot parent (org + domain from the key).
375
+ if (ctx.positionals[0] === "domain") {
376
+ return cmdOnboardDomain({ ...ctx, positionals: ctx.positionals.slice(1) });
377
+ }
225
378
  // `onboard sync-sitemap --collection <uuid>` — refresh an already-onboarded blog.
226
379
  if (ctx.positionals[0] === "sync-sitemap") {
227
380
  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.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",