@letterstory/cli 0.19.0 → 0.23.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
@@ -287,6 +287,18 @@ single fallback plan, the client's existing publications with an overlap warning
287
287
  custom-domain headroom. `mastheads` returns names that never carry the client's own name. Without
288
288
  `--plan-file`, `phantom-onboard <domain>` still plans from the homepage alone.
289
289
 
290
+ To review a plan and then apply **that** plan, hand it back with `--accept-plan`. A bare
291
+ onboarding computes the plan twice — once for the dry run you read, once inside the job — and
292
+ the second masthead proposal need not match the first, so the name committed into the URL
293
+ would not be the one you approved. `--accept-plan` pins it, implies `--accept`, and skips the
294
+ second homepage read.
295
+
296
+ ```bash
297
+ letterstory phantom-onboard acme.com --json > plan.json # dry run: read it, edit nothing
298
+ letterstory phantom-onboard acme.com --accept-plan plan.json
299
+ jq '.plan' plan.json | letterstory phantom-onboard acme.com --accept-plan -
300
+ ```
301
+
290
302
  ## Content series
291
303
 
292
304
  A series is a coverage obligation, not a keyword bet: "content that looks like this",
@@ -326,6 +338,34 @@ letterstory kernel run --article <uuid> --kernel <uuid> [--brief "…" | --brief
326
338
  letterstory kernel status <kernel-job-id>
327
339
  ```
328
340
 
341
+ ### The standalone kernel endpoint
342
+
343
+ `kernel write` and `kernel polish` hit the standalone door: prose comes back, nothing is
344
+ saved into an article, and it is metered in kernel-run credits rather than the AI-usage gate.
345
+
346
+ ```bash
347
+ letterstory kernel endpoint create --name "Writer"
348
+ letterstory kernel endpoint delete <endpoint-id> --yes
349
+
350
+ letterstory kernel write <endpoint-id> --file outline.md \
351
+ --brief-mode outline --person first --author "Ada" --shredder <endpoint-id>
352
+ # Follows the run and prints the ARTICLE when it lands. --no-wait returns the run id instead.
353
+ ```
354
+
355
+ ### Shredding
356
+
357
+ ```bash
358
+ letterstory shredder run <endpoint-id> --file post.md --format markdown
359
+ # Queues, follows the job, prints the finished DOCUMENT. Exits non-zero if the job lands
360
+ # as error or degraded, so a script can branch on it without parsing the payload.
361
+
362
+ letterstory shredder run <endpoint-id> --file post.md --no-wait # just queue it
363
+ letterstory shredder job <job-id> # poll it yourself
364
+ ```
365
+
366
+ Both follow loops poll every 15–20 s. `LETTERSTORY_POLL_INTERVAL_MS` overrides that — set
367
+ it to `0` in CI so a test does not wait on a real interval.
368
+
329
369
  `phantom-job create --topic "…" --collection <uuid>` runs topic → draft → publish → rebuild
330
370
  end to end, with `phantom-job status <job-id>` to follow it.
331
371
 
package/lib/cli.mjs CHANGED
@@ -44,7 +44,7 @@ import {
44
44
  } from "./commands.mjs";
45
45
 
46
46
  // Keep in sync with cli/package.json.
47
- export const VERSION = "0.19.0";
47
+ export const VERSION = "0.23.0";
48
48
 
49
49
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
50
50
  // can't accidentally swallow the id as --json's value.
@@ -82,9 +82,19 @@ const BOOLEAN_FLAGS = new Set([
82
82
  "no-visuals",
83
83
  "no-enhance",
84
84
  "no-key",
85
+ // `collections activate <id> --no-topic-map` / `--no-headlines`
86
+ "no-topic-map",
87
+ "no-headlines",
85
88
  "onboard",
86
89
  "test",
87
90
  "real",
91
+ // `phantom-onboard collection --enable-planner`, `strategy sitemap --posts-only`, and the
92
+ // `strategy positioning set --clear-*` switches: value-less, and each sits next to a value.
93
+ "enable-planner",
94
+ "posts-only",
95
+ "clear-topics",
96
+ "clear-stances",
97
+ "clear-avoid",
88
98
  ]);
89
99
 
90
100
  // Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
@@ -194,9 +204,12 @@ Reviewers:
194
204
  Collections:
195
205
  collections list List your collections
196
206
  collections new --name <n> [--description <text>]
197
- collections update <id> [--name] [--description] [--cadence-target <n>] [--cadence-period week|month] [--cover-canvas-type-id <uuid>|none]
207
+ collections update <id> [--name] [--description] [--cadence-target <n>] [--cadence-period week|month]
208
+ [--cover-canvas-type-id <uuid>|none] [--seo-market <cc>]
198
209
  collections delete <id> --yes
199
210
  collections assign <article-id> <collection-id>
211
+ collections activate <id> [--cadence <n>] [--approval manual|review_window|autopilot] [--no-topic-map] [--no-headlines]
212
+ The review is done: topic map from the areas, first headlines, Planner on, queue filled
200
213
  collections autopen <id> [--template <key>|none] [--kernel-family <name>|org]
201
214
  [--mention off|subtle|woven] [--outline-depth barebones|fleshed_out]
202
215
  [--direction-gate true|false] [--auto-start true|false]
@@ -245,11 +258,11 @@ Connectors:
245
258
  Wire a collection to a CMS + field mapping
246
259
 
247
260
  Strategy & onboarding:
248
- strategy company get | set [--name] [--domain] [--manifesto <text>|--manifesto-file <path>]
261
+ strategy company get | set [--name] [--domain] [--market <cc>] [--manifesto <text>|--manifesto-file <path>]
249
262
  strategy positioning get | set [--topic <t> …] [--stance <s> …] [--avoid <t> …]
250
263
  [--clear-topics] [--clear-stances] [--clear-avoid]
251
264
  strategy competitors list | add <name> <domain>
252
- strategy sitemap --collection <uuid> --url <sitemap_url> [--sub <url> …] [--pattern <glob>]
265
+ strategy sitemap --collection <uuid> --url <sitemap_url> [--sub <url> …] [--pattern <glob>] [--posts-only]
253
266
  strategy topics list --collection <uuid> List the topic map's clusters/spokes
254
267
  strategy topics set --collection <uuid> (--topic <topic-id> | --suggestion <suggestion-id>)
255
268
  suggestions list <collection_id> List pending strategy suggestions + their auto-apply countdown
@@ -298,14 +311,16 @@ Writing kernels (kernels write the draft; they do not research — run research
298
311
 
299
312
  Standalone — prose back, nothing saved into an article (metered in kernel-run credits):
300
313
  kernel endpoints Your kernel endpoints
314
+ kernel endpoint create --name <n> [--kernel <uuid>] [--shredder <endpoint-id>]
315
+ New kernel endpoint, optionally bound
316
+ to a kernel and an in-loop shredder
317
+ kernel endpoint delete <endpoint-id> --yes Remove one
301
318
  kernel write <endpoint-id> (--body <text>|--file <path>|--file -)
302
- [--kernel <uuid>] [--words <n>] [--no-wait]
303
- [--shredder <uuid>] shred every round in the loop, so the
304
- kernel judges the text that ships
305
- [--read-as brief|blog|outline|research|notes]
319
+ export const VERSION = "0.23.0";
306
320
  WRITE a new article from a headline,
307
321
  outline, notes or a finished post.
308
- Follows the run unless --no-wait.
322
+ Follows the run unless --no-wait, and
323
+ prints the article when it lands.
309
324
  kernel polish <endpoint-id> (--body <html>|--file <path>) [--format auto|html]
310
325
  POLISH the document you have: prose
311
326
  reworded, your own markup kept
@@ -336,6 +351,13 @@ Phantom onboarding recipe (domain -> live phantom blog with first posts):
336
351
  lane below: its direction AND pillars are
337
352
  applied verbatim, so the site covers what
338
353
  was approved.
354
+ [--accept-plan <path|->] Apply a plan you already reviewed,
355
+ verbatim — the "plan" from a dry run, or
356
+ a whole saved --json response. Implies
357
+ --accept and skips re-planning, so the
358
+ masthead you approved is the one committed
359
+ (without it the apply re-reads the homepage
360
+ and may propose a different name).
339
361
 
340
362
  Propose lane (the Fleet wizard's Plan and Name screens; persists nothing, spends nothing):
341
363
  ${bin === "phantom" ? "onboard" : "phantom-onboard"} propose [<client-domain>] [--about "the beat"] [--name X] [--json]
@@ -353,9 +375,19 @@ Propose lane (the Fleet wizard's Plan and Name screens; persists nothing, spends
353
375
  --plan-id picks the plan) or a single plan
354
376
  (--plan '<json>' inline, or - for stdin).
355
377
 
378
+ What does this org still need? (the router — reads only, decides nothing):
379
+ ${bin === "phantom" ? "onboard" : "phantom-onboard"} plan Every onboarding step (context, main blog,
380
+ Planner, CMS, phantom) with its status:
381
+ done / running / ready / blocked, and what
382
+ a blocked step is waiting on.
383
+ ${bin === "phantom" ? "onboard" : "phantom-onboard"} next The first ready step Letterstory recommends,
384
+ as the exact command to run next. Optional
385
+ steps are listed, never chosen.
386
+
356
387
  Collection onboarding (your OWN blog, on your org's domain — no provisioning):
357
388
  ${bin === "phantom" ? "onboard" : "phantom-onboard"} collection [--collection <uuid>] [--name "<collection name>"]
358
- [--about "the direction"] [--sitemap <url>] [--cms <provider>] [--watch]
389
+ [--about "the direction"] [--sitemap <url>] [--cms <provider>]
390
+ [--enable-planner [--cadence <n/week>] [--approval manual|review_window|autopilot]] [--watch]
359
391
  Import the existing blog from its sitemap
360
392
  (auto-discovered from the org's domain unless
361
393
  --sitemap pins one) OR from a connected CMS with
@@ -363,7 +395,12 @@ Collection onboarding (your OWN blog, on your org's domain — no provisioning):
363
395
  derive content areas + series, build the topic
364
396
  map, bind SEO demand, and stock Headlines & Queue.
365
397
  Omit --collection to create a fresh one (--name,
366
- default Blog). The Planner stays OFF (manual approval).
398
+ default Blog). The Planner stays OFF unless you
399
+ pass --enable-planner, which turns it on at the
400
+ chosen cadence and approval and fills the queue
401
+ (same activation as the app's Guided review step;
402
+ needs a user-scoped key). --approval defaults to
403
+ manual: nothing publishes unreviewed.
367
404
  ${bin === "phantom" ? "onboard" : "phantom-onboard"} sync-sitemap --collection <uuid>
368
405
  Re-check an already-onboarded blog's sitemap
369
406
  right now instead of waiting for its cadence
@@ -449,6 +486,9 @@ Shredder (diversify content produced outside Letterstory; structure preserved):
449
486
  shredder create --name <n> New endpoint (returns a callable public id)
450
487
  shredder delete <endpoint-id> --yes
451
488
  shredder run <endpoint-id> (--body <text>|--file <path>|--file -) [--coverage <0..1>] [--attempts <n>]
489
+ [--format auto|html|markdown]
490
+ what the text IS; 'auto' reads a document
491
+ containing block-level HTML as HTML
452
492
  [--aggression standard|high|max]
453
493
  standard (default) keeps the piece's own terminology;
454
494
  high rewrites more and may reword recurring terms;
@@ -462,8 +502,11 @@ Shredder (diversify content produced outside Letterstory; structure preserved):
462
502
  [--sync] wait inside one request (only for a few
463
503
  sentences — anything longer is cut off)
464
504
  Shred prose through an endpoint (logs telemetry).
465
- Queues the work and follows it; a shred takes minutes
466
- and keeps running on the server if you stop waiting.
505
+ Queues the work and follows it, printing the finished
506
+ document; a shred takes minutes and keeps running on
507
+ the server if you stop waiting. Exits non-zero if the
508
+ job lands as error or degraded. Pace the polling with
509
+ LETTERSTORY_POLL_INTERVAL_MS.
467
510
  shredder job <job-id> Poll a queued shred and print it when it lands
468
511
  shredder runs [--endpoint <id>] [--limit <n>] Recent shred calls (telemetry), newest first
469
512
 
@@ -2,7 +2,82 @@
2
2
  // Ported from phantomstory-cli's collections.ts.
3
3
 
4
4
  import { CliError } from "../client.mjs";
5
- import { flagStr, flagNum, requireFlag, requirePositional, printResult, compact, ok } from "./shared.mjs";
5
+ import { flagStr, printResult, ok, argsFromFlags } from "./shared.mjs";
6
+
7
+ // `--flag none|off|""` clears a nullable field; the server takes null for that.
8
+ const clearable = (flag) => ({
9
+ value: (flags) => {
10
+ const v = flagStr(flags[flag]);
11
+ if (v === undefined) return undefined;
12
+ return ["none", "off", ""].includes(v.toLowerCase()) ? null : v;
13
+ },
14
+ });
15
+
16
+ // A `--flag false` boolean (the value is the string "false"; anything else is true).
17
+ const stringBool = (flag) => ({
18
+ value: (flags) => (flags[flag] === undefined ? undefined : flags[flag] !== "false"),
19
+ });
20
+
21
+ // How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
22
+ // `scripts/parity-check.ts` compares these keys against each tool's published input schema.
23
+ export const ARGS = {
24
+ list_collections: {},
25
+ create_collection: {
26
+ name: { flag: "name", required: true },
27
+ description: "description",
28
+ },
29
+ update_collection: {
30
+ collection_id: { positional: 0, label: "collection-id", required: true },
31
+ name: "name",
32
+ description: "description",
33
+ cadence_target: { flag: "cadence-target", type: "num" },
34
+ cadence_period: { flag: "cadence-period", enum: ["week", "month"] },
35
+ cover_canvas_type_id: clearable("cover-canvas-type-id"),
36
+ seo_market: "seo-market",
37
+ },
38
+ delete_collection: {
39
+ collection_id: { positional: 0, label: "collection-id", required: true },
40
+ },
41
+ assign_article_to_collection: {
42
+ article_id: { positional: 0, label: "article-id", required: true },
43
+ collection_id: { positional: 1, label: "collection-id", required: true },
44
+ },
45
+ // `collections autopen <id>` — a PATCH: only the flags given are sent.
46
+ // `collections activate <id>` — the review is done, turn the Planner on. Omitted
47
+ // knobs keep the collection's saved values, so nothing here carries a default:
48
+ // an absent flag must stay absent rather than becoming an opinion (see #1773).
49
+ activate_collection: {
50
+ collection_id: { positional: 0, label: "collection-id", required: true },
51
+ cadence_per_week: { flag: "cadence", type: "num" },
52
+ approval_mode: { flag: "approval", enum: ["manual", "review_window", "autopilot"] },
53
+ // `--no-topic-map` / `--no-headlines` mean "skip that step"; absent means "do it".
54
+ regenerate_topic_map: { value: (flags) => (flags["no-topic-map"] === undefined ? undefined : false) },
55
+ stock_headlines: { value: (flags) => (flags["no-headlines"] === undefined ? undefined : false) },
56
+ },
57
+ update_auto_pen_config: {
58
+ collection_id: { positional: 0, label: "collection-id", required: true },
59
+ // `--template none` clears the binding; the server takes null for that.
60
+ template_key: clearable("template"),
61
+ outline: {
62
+ object: {
63
+ depth: "outline-depth",
64
+ direction_gate: stringBool("direction-gate"),
65
+ },
66
+ },
67
+ kernel: {
68
+ object: {
69
+ family: {
70
+ value: (flags) => {
71
+ const v = flagStr(flags["kernel-family"]);
72
+ return v === undefined ? undefined : v === "org" ? null : v;
73
+ },
74
+ },
75
+ },
76
+ },
77
+ mention: { object: { degree: "mention" } },
78
+ auto_start_on_curate: stringBool("auto-start"),
79
+ },
80
+ };
6
81
 
7
82
  export async function cmdCollections(ctx) {
8
83
  const sub = ctx.positionals[0];
@@ -23,13 +98,38 @@ export async function cmdCollections(ctx) {
23
98
  return collectionsAssign(rest);
24
99
  case "autopen":
25
100
  return collectionsAutoPen(rest);
101
+ case "activate":
102
+ return collectionsActivate(rest);
26
103
  default:
27
104
  throw new CliError(
28
- `Unknown collections subcommand: ${sub ?? "(none)"}. Try: list, new, update, delete, assign, autopen`
105
+ `Unknown collections subcommand: ${sub ?? "(none)"}. Try: list, new, update, delete, assign, autopen, activate`
29
106
  );
30
107
  }
31
108
  }
32
109
 
110
+ /**
111
+ * `collections activate <id>` — the review is done: build the topic map from the
112
+ * collection's content areas, seed headlines, turn the Planner on with the queue
113
+ * filled. The same activation the app's Guided review step runs on Continue.
114
+ * Omitted knobs keep the collection's saved values; the response says, per knob,
115
+ * whether it came from you, from the saved config, or from the default.
116
+ */
117
+ async function collectionsActivate(ctx) {
118
+ const { client, positionals, flags, io } = ctx;
119
+ const args = argsFromFlags(ARGS.activate_collection, flags, positionals);
120
+ const result = await client.callTool("activate_collection", args);
121
+ if (result && result.activated === false) {
122
+ // The K1 floor, as data: say why, and name the tools that clear it.
123
+ io.error(`Not activated: ${result.reason}`);
124
+ for (const r of result.remedies ?? []) io.error(` ${r.tool} — ${r.why}`);
125
+ printResult(io, flags, result);
126
+ return 1;
127
+ }
128
+ ok(ctx, result?.note ?? `Activated collection ${args.collection_id}.`);
129
+ printResult(io, flags, result);
130
+ return 0;
131
+ }
132
+
33
133
  async function collectionsList(ctx) {
34
134
  const { client, flags, io } = ctx;
35
135
  const result = await client.callTool("list_collections", {});
@@ -39,35 +139,19 @@ async function collectionsList(ctx) {
39
139
 
40
140
  async function collectionsNew(ctx) {
41
141
  const { client, flags, io } = ctx;
42
- const name = requireFlag(flags, "name");
43
- const args = compact({ name, description: flagStr(flags.description) });
142
+ const args = argsFromFlags(ARGS.create_collection, flags);
44
143
  const result = await client.callTool("create_collection", args);
45
- ok(ctx, `Created collection "${name}".`);
144
+ ok(ctx, `Created collection "${args.name}".`);
46
145
  printResult(io, flags, result);
47
146
  return 0;
48
147
  }
49
148
 
50
149
  async function collectionsUpdate(ctx) {
51
150
  const { client, positionals, flags, io } = ctx;
52
- const id = requirePositional(positionals, 0, "collection-id");
53
- const coverCanvasTypeId = flagStr(flags["cover-canvas-type-id"]);
54
- const patch = compact({
55
- collection_id: id,
56
- name: flagStr(flags.name),
57
- description: flagStr(flags.description),
58
- cadence_target: flagNum(flags["cadence-target"]),
59
- cadence_period: flagStr(flags["cadence-period"]),
60
- });
61
- if (coverCanvasTypeId !== undefined) {
62
- // compact() drops undefined, not null — set it after so "none"/"off" can clear
63
- // the field instead of being dropped. Same convention as `webhook set --url none`.
64
- patch.cover_canvas_type_id = ["none", "off", ""].includes(coverCanvasTypeId.toLowerCase())
65
- ? null
66
- : coverCanvasTypeId;
67
- }
151
+ const patch = argsFromFlags(ARGS.update_collection, flags, positionals);
68
152
  if (Object.keys(patch).length <= 1) {
69
153
  throw new CliError(
70
- "Nothing to update — pass at least one of --name/--description/--cadence-target/--cadence-period/--cover-canvas-type-id."
154
+ "Nothing to update — pass at least one of --name/--description/--cadence-target/--cadence-period/--cover-canvas-type-id/--seo-market."
71
155
  );
72
156
  }
73
157
  const result = await client.callTool("update_collection", patch);
@@ -78,12 +162,12 @@ async function collectionsUpdate(ctx) {
78
162
 
79
163
  async function collectionsDelete(ctx) {
80
164
  const { client, positionals, flags, io } = ctx;
81
- const id = requirePositional(positionals, 0, "collection-id");
165
+ const args = argsFromFlags(ARGS.delete_collection, flags, positionals);
82
166
  if (!flags.yes) {
83
- io.error(`Re-run with --yes to confirm: ${ctx.bin} collections delete ${id} --yes`);
167
+ io.error(`Re-run with --yes to confirm: ${ctx.bin} collections delete ${args.collection_id} --yes`);
84
168
  return 1;
85
169
  }
86
- const result = await client.callTool("delete_collection", { collection_id: id });
170
+ const result = await client.callTool("delete_collection", args);
87
171
  ok(ctx, "Collection deleted.");
88
172
  printResult(io, flags, result);
89
173
  return 0;
@@ -91,12 +175,10 @@ async function collectionsDelete(ctx) {
91
175
 
92
176
  async function collectionsAssign(ctx) {
93
177
  const { client, positionals, flags, io } = ctx;
94
- const articleId = requirePositional(positionals, 0, "article-id");
95
- const collectionId = requirePositional(positionals, 1, "collection-id");
96
- const result = await client.callTool("assign_article_to_collection", {
97
- article_id: articleId,
98
- collection_id: collectionId,
99
- });
178
+ const result = await client.callTool(
179
+ "assign_article_to_collection",
180
+ argsFromFlags(ARGS.assign_article_to_collection, flags, positionals)
181
+ );
100
182
  ok(ctx, "Article assigned.");
101
183
  printResult(io, flags, result);
102
184
  return 0;
@@ -111,27 +193,7 @@ async function collectionsAssign(ctx) {
111
193
  */
112
194
  async function collectionsAutoPen(ctx) {
113
195
  const { client, positionals, flags, io } = ctx;
114
- const collectionId = requirePositional(positionals, 0, "collection-id");
115
- const patch = { collection_id: collectionId };
116
-
117
- // `--template none` clears the binding; the server takes null for that.
118
- const template = flagStr(flags.template);
119
- if (template !== undefined) patch.template_key = template === "none" ? null : template;
120
-
121
- const kernelFamily = flagStr(flags["kernel-family"]);
122
- if (kernelFamily !== undefined) patch.kernel = { family: kernelFamily === "org" ? null : kernelFamily };
123
-
124
- const mention = flagStr(flags.mention);
125
- if (mention !== undefined) patch.mention = { degree: mention };
126
-
127
- const depth = flagStr(flags["outline-depth"]);
128
- if (depth !== undefined) patch.outline = { ...(patch.outline ?? {}), depth };
129
-
130
- if (flags["direction-gate"] !== undefined) {
131
- patch.outline = { ...(patch.outline ?? {}), direction_gate: flags["direction-gate"] !== "false" };
132
- }
133
- if (flags["auto-start"] !== undefined) patch.auto_start_on_curate = flags["auto-start"] !== "false";
134
-
196
+ const patch = argsFromFlags(ARGS.update_auto_pen_config, flags, positionals);
135
197
  if (Object.keys(patch).length === 1) {
136
198
  throw new CliError(
137
199
  "Nothing to change. Pass at least one of --template, --kernel-family, --mention, --outline-depth, --direction-gate, --auto-start."
@@ -139,7 +201,7 @@ async function collectionsAutoPen(ctx) {
139
201
  }
140
202
 
141
203
  const result = await client.callTool("update_auto_pen_config", patch);
142
- ok(ctx, `Auto-Pen settings updated for ${collectionId}.`);
204
+ ok(ctx, `Auto-Pen settings updated for ${patch.collection_id}.`);
143
205
  printResult(io, flags, result);
144
206
  return 0;
145
207
  }
@@ -3,7 +3,62 @@
3
3
  // phantomstory-cli's connectors.ts.
4
4
 
5
5
  import { CliError } from "../client.mjs";
6
- import { flagStr, flagBool, requireFlag, requirePositional, printResult, compact, ok } from "./shared.mjs";
6
+ import { printResult, ok, argsFromFlags } from "./shared.mjs";
7
+
8
+ const CMS_PROVIDERS = ["webflow", "framer", "sanity", "contentful"];
9
+ const PUBLISH_CONNECTORS = ["webflow", "framer", "google_docs", "contentful"];
10
+
11
+ // How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
12
+ // `scripts/parity-check.ts` compares these keys against each tool's published input schema.
13
+ export const ARGS = {
14
+ list_publish_targets: {},
15
+ publish_to_connector: {
16
+ article_id: { positional: 0, label: "article-id", required: true },
17
+ connector: { flag: "to", required: true, enum: PUBLISH_CONNECTORS },
18
+ target_id: { flag: "target", required: true },
19
+ flow_run_id: "flow-run",
20
+ // A re-publish whose external item was deleted or unpublished in the client's CMS
21
+ // is refused, and the refusal explains itself. --confirm-missing-target is the
22
+ // answer to that specific question: it re-creates / re-publishes the item there.
23
+ // Deliberately not bundled into a general --yes, and never set by default.
24
+ confirm_missing_target: { flag: "confirm-missing-target", type: "bool" },
25
+ },
26
+ get_publish_status: {
27
+ connector: { flag: "connector", required: true, enum: PUBLISH_CONNECTORS },
28
+ publish_id: { flag: "publish-id", required: true },
29
+ },
30
+ import_from_cms: {
31
+ collection_id: "collection",
32
+ provider: { flag: "provider", enum: CMS_PROVIDERS },
33
+ },
34
+ connect_cms: {
35
+ provider: { flag: "provider", required: true, enum: CMS_PROVIDERS },
36
+ // api_token is required for every provider; the rest are provider-specific.
37
+ api_token: { flag: "token", required: true },
38
+ project_url: "project-url",
39
+ auto_deploy: { flag: "auto-deploy", type: "bool", negate: "no-auto-deploy" },
40
+ project_id: "project-id",
41
+ dataset: "dataset",
42
+ project_name: "project-name",
43
+ workspace_name: "workspace",
44
+ space_id: "space-id",
45
+ environment_id: "environment",
46
+ space_name: "space-name",
47
+ default_locale: "locale",
48
+ },
49
+ disconnect_cms: {
50
+ provider: { flag: "provider", required: true, enum: CMS_PROVIDERS },
51
+ },
52
+ list_cms_connections: {},
53
+ list_cms_mappings: {
54
+ provider: { flag: "provider", required: true, enum: CMS_PROVIDERS },
55
+ },
56
+ bind_collection_cms: {
57
+ collection_id: { flag: "collection", required: true },
58
+ provider: { flag: "provider", required: true, enum: CMS_PROVIDERS },
59
+ mapping_id: "mapping",
60
+ },
61
+ };
7
62
 
8
63
  export async function cmdConnectors(ctx) {
9
64
  const sub = ctx.positionals[0];
@@ -46,31 +101,16 @@ async function connectorsList(ctx) {
46
101
 
47
102
  async function connectorsPublish(ctx) {
48
103
  const { client, positionals, flags, io } = ctx;
49
- const articleId = requirePositional(positionals, 0, "article-id");
50
- const to = requireFlag(flags, "to");
51
- const target = requireFlag(flags, "target");
52
- // A re-publish whose external item was deleted or unpublished in the client's CMS
53
- // is refused, and the refusal explains itself. --confirm-missing-target is the
54
- // answer to that specific question: it re-creates / re-publishes the item there.
55
- // Deliberately not bundled into a general --yes, and never set by default.
56
- const args = compact({
57
- article_id: articleId,
58
- connector: to,
59
- target_id: target,
60
- flow_run_id: flagStr(flags["flow-run"]),
61
- confirm_missing_target: flagBool(flags["confirm-missing-target"]) || undefined,
62
- });
104
+ const args = argsFromFlags(ARGS.publish_to_connector, flags, positionals);
63
105
  const result = await client.callTool("publish_to_connector", args);
64
- ok(ctx, `Publishing to ${to}.`);
106
+ ok(ctx, `Publishing to ${args.connector}.`);
65
107
  printResult(io, flags, result);
66
108
  return 0;
67
109
  }
68
110
 
69
111
  async function connectorsStatus(ctx) {
70
112
  const { client, flags, io } = ctx;
71
- const connector = requireFlag(flags, "connector");
72
- const publishId = requireFlag(flags, "publish-id");
73
- const result = await client.callTool("get_publish_status", { connector, publish_id: publishId });
113
+ const result = await client.callTool("get_publish_status", argsFromFlags(ARGS.get_publish_status, flags));
74
114
  printResult(io, flags, result);
75
115
  return 0;
76
116
  }
@@ -79,16 +119,14 @@ async function connectorsPull(ctx) {
79
119
  const { client, flags, io } = ctx;
80
120
  // Dual key: --collection (a CMS-bound collection), or --provider (resolve/wire),
81
121
  // or both (--provider wires an unbound --collection first). At least one required.
82
- const collectionId = flagStr(flags.collection);
83
- const provider = flagStr(flags.provider);
84
- if (!collectionId && !provider) {
122
+ const args = argsFromFlags(ARGS.import_from_cms, flags);
123
+ if (!args.collection_id && !args.provider) {
85
124
  throw new CliError("Pass --collection <id> and/or --provider <webflow|framer|sanity|contentful>");
86
125
  }
87
- const args = compact({ collection_id: collectionId, provider });
88
126
  const result = await client.callTool("import_from_cms", args);
89
127
  ok(
90
128
  ctx,
91
- `Pulled from ${result?.provider ?? provider ?? "CMS"} into collection ${result?.collection_id ?? collectionId ?? ""}.`
129
+ `Pulled from ${result?.provider ?? args.provider ?? "CMS"} into collection ${result?.collection_id ?? args.collection_id ?? ""}.`
92
130
  );
93
131
  printResult(io, flags, result);
94
132
  return 0;
@@ -96,33 +134,18 @@ async function connectorsPull(ctx) {
96
134
 
97
135
  async function connectorsConnect(ctx) {
98
136
  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
- });
137
+ const args = argsFromFlags(ARGS.connect_cms, flags);
115
138
  const result = await client.callTool("connect_cms", args);
116
- ok(ctx, `Connected ${provider}${result?.label ? ` (${result.label})` : ""}.`);
139
+ ok(ctx, `Connected ${args.provider}${result?.label ? ` (${result.label})` : ""}.`);
117
140
  printResult(io, flags, result);
118
141
  return 0;
119
142
  }
120
143
 
121
144
  async function connectorsDisconnect(ctx) {
122
145
  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}.`);
146
+ const args = argsFromFlags(ARGS.disconnect_cms, flags);
147
+ const result = await client.callTool("disconnect_cms", args);
148
+ ok(ctx, `Disconnected ${args.provider}.`);
126
149
  printResult(io, flags, result);
127
150
  return 0;
128
151
  }
@@ -136,19 +159,14 @@ async function connectorsConnections(ctx) {
136
159
 
137
160
  async function connectorsMappings(ctx) {
138
161
  const { client, flags, io } = ctx;
139
- const provider = requireFlag(flags, "provider");
140
- const result = await client.callTool("list_cms_mappings", { provider });
162
+ const result = await client.callTool("list_cms_mappings", argsFromFlags(ARGS.list_cms_mappings, flags));
141
163
  printResult(io, flags, result);
142
164
  return 0;
143
165
  }
144
166
 
145
167
  async function connectorsBind(ctx) {
146
168
  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
- });
169
+ const args = argsFromFlags(ARGS.bind_collection_cms, flags);
152
170
  const result = await client.callTool("bind_collection_cms", args);
153
171
  ok(
154
172
  ctx,