@letterstory/cli 0.84.0 → 0.85.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
@@ -644,6 +644,10 @@ letterstory org join acme/ > acme.json # rebuild the identical bundl
644
644
  letterstory org diff acme.json # what applying the bundle would change: + add, ~ change, - only in the org
645
645
  letterstory org apply acme.json --dry-run # what apply would write; then drop --dry-run to apply
646
646
  letterstory org apply acme.json --prune --yes # also delete records the bundle lacks
647
+ letterstory org git connect acme/config --path letterstory/org.json # keep the config in GitHub: each new commit is planned and posted to Slack
648
+ letterstory org git connect acme/config --mode apply # …or applied (deletes only with --prune)
649
+ letterstory org git status # the connected repo and its last sync
650
+ letterstory org git sync [--force] # sync now instead of waiting for the 15-minute poll
647
651
  ```
648
652
 
649
653
  ## Seers — event-driven signals
package/lib/cli.mjs CHANGED
@@ -57,7 +57,7 @@ import {
57
57
  } from "./commands.mjs";
58
58
 
59
59
  // Keep in sync with cli/package.json.
60
- export const VERSION = "0.84.0";
60
+ export const VERSION = "0.85.0";
61
61
 
62
62
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
63
63
  // can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
@@ -77,6 +77,8 @@ export const BOOLEAN_FLAGS = new Set([
77
77
  "no-color",
78
78
  "dry-run",
79
79
  "prune",
80
+ // org git sync --force
81
+ "force",
80
82
  "buy",
81
83
  "rebuild",
82
84
  "print-key",
@@ -963,7 +965,9 @@ Sequences (multi-step cold email sent from your mailers; approving or enrolling
963
965
  [--[no-]opt-out-footer] [--[no-]postal-address-footer] [--[no-]unsubscribe-links] [--[no-]detect-reply-opt-outs]
964
966
  [--[no-]test-mode] [--default-lede <fallback opener line>]
965
967
  [--send-window-start <HH:MM>] [--send-window-end <HH:MM>] [--send-window-timezone <IANA tz>]
968
+ [--enroll-on-cake paid|scheduled|out_for_delivery|delivered|none]
966
969
  --test-mode (internal accounts only): walks as live, sends nothing
970
+ --enroll-on-cake: start it when a recipient's Daymaker cake reaches that stage
967
971
  sequences pause|resume <sequence-id> Stop / restart sending
968
972
  sequences enroll <sequence-id> (--list <list-id> | --email <addr>... | --recipients '<json>'|--recipients-file <path>)
969
973
  sequences stop <sequence-id> <enrollment-id> Stop one recipient
@@ -973,6 +977,10 @@ Sequences (multi-step cold email sent from your mailers; approving or enrolling
973
977
  sequences links list Live Pocket Dispatch links (no tokens)
974
978
  sequences links create --days <1..3> Mint a no-login review link (shown once)
975
979
  sequences links revoke <link-id> --yes
980
+ sequences cakes [--stage <stage>] [--limit <n>] Daymaker cakes: stage, matched prospect, sequences started
981
+ sequences cakes sync Re-read the connected Daymaker account now
982
+ sequences cakes connect --daymaker-key <dm_live_…> Connect the org's Daymaker account (key checked, stored encrypted)
983
+ sequences cakes disconnect Forget the key and remove the webhook
976
984
 
977
985
  Dispatches (the named review desks a sequence's review stage queues to):
978
986
  dispatches list Desks + how many emails each has queued
@@ -23,6 +23,18 @@ export const ARGS = {
23
23
  dry_run: { flag: "dry-run", type: "bool" },
24
24
  prune: { flag: "prune", type: "bool" },
25
25
  },
26
+ get_org_config_source: {},
27
+ connect_org_config_source: {
28
+ repo: { positional: 0, label: "owner/name" },
29
+ branch: { flag: "branch" },
30
+ path: { flag: "path" },
31
+ mode: { flag: "mode" },
32
+ prune: { flag: "prune", type: "bool" },
33
+ },
34
+ sync_org_config_source: {
35
+ force: { flag: "force", type: "bool" },
36
+ },
37
+ disconnect_org_config_source: {},
26
38
  };
27
39
 
28
40
  export async function cmdOrg(ctx) {
@@ -37,8 +49,10 @@ export async function cmdOrg(ctx) {
37
49
  return orgDiff(rest);
38
50
  case "apply":
39
51
  return orgApply(rest);
52
+ case "git":
53
+ return orgGit(rest);
40
54
  default:
41
- throw new CliError(`Unknown org subcommand: ${sub ?? "(none)"}. Try: export, join, diff, apply`);
55
+ throw new CliError(`Unknown org subcommand: ${sub ?? "(none)"}. Try: export, join, diff, apply, git`);
42
56
  }
43
57
  }
44
58
 
@@ -220,3 +234,70 @@ async function orgApply(ctx) {
220
234
  printResult(io, flags, result, formatApply);
221
235
  return result.failed.length ? 1 : 0;
222
236
  }
237
+
238
+ export function formatSource(source) {
239
+ if (!source) return "No config repo connected. Connect one with `org git connect <owner/name>`.";
240
+ const lines = [
241
+ `${source.repo}@${source.branch}:${source.path} — mode ${source.mode}${source.prune ? " (prune)" : ""}`,
242
+ ` last seen: ${source.last_seen_sha?.slice(0, 7) ?? "—"}${source.last_synced_at ? ` at ${source.last_synced_at}` : ""}`,
243
+ ` last applied: ${source.last_applied_sha?.slice(0, 7) ?? "—"}`,
244
+ ];
245
+ if (source.last_error) lines.push(` last error: ${source.last_error}`);
246
+ return lines.join("\n");
247
+ }
248
+
249
+ function formatSync(outcome) {
250
+ const at = outcome.sha ? outcome.sha.slice(0, 7) : "?";
251
+ if (outcome.status === "unchanged")
252
+ return `Already synced ${at} — nothing new on the branch (use --force to re-run it).`;
253
+ if (outcome.status === "error") return `Sync failed at ${at}: ${outcome.error}`;
254
+ if (outcome.status === "planned") return `Planned ${at} (plan mode, nothing applied):\n${formatPlan(outcome.plan)}`;
255
+ return `Applied ${at}:\n${formatApply(outcome.result)}`;
256
+ }
257
+
258
+ async function orgGit(ctx) {
259
+ const { client, flags, io } = ctx;
260
+ const sub = ctx.positionals[0];
261
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
262
+ switch (sub) {
263
+ case "status": {
264
+ refuseUnknownFlags(rest, ARGS.get_org_config_source);
265
+ const { source } = await client.callTool("get_org_config_source", {});
266
+ printResult(io, flags, source, formatSource);
267
+ return 0;
268
+ }
269
+ case "connect": {
270
+ refuseUnknownFlags(rest, ARGS.connect_org_config_source);
271
+ const out = await client.callTool(
272
+ "connect_org_config_source",
273
+ argsFromFlags(ARGS.connect_org_config_source, flags, rest.positionals)
274
+ );
275
+ printResult(io, flags, out, (o) =>
276
+ [
277
+ `Connected ${o.org} to ${formatSource(o.source)}`,
278
+ `The next sync ${o.source.mode === "apply" ? "applies" : "plans"} ${o.head.slice(0, 7)} — run \`org git sync\` to do it now.`,
279
+ ].join("\n")
280
+ );
281
+ return 0;
282
+ }
283
+ case "sync": {
284
+ refuseUnknownFlags(rest, ARGS.sync_org_config_source);
285
+ const { outcome } = await client.callTool(
286
+ "sync_org_config_source",
287
+ argsFromFlags(ARGS.sync_org_config_source, flags)
288
+ );
289
+ printResult(io, flags, outcome, formatSync);
290
+ return outcome.status === "error" ? 1 : 0;
291
+ }
292
+ case "disconnect": {
293
+ refuseUnknownFlags(rest, ARGS.disconnect_org_config_source);
294
+ const { disconnected } = await client.callTool("disconnect_org_config_source", {});
295
+ ok(ctx, disconnected ? "Disconnected the config repo." : "No config repo was connected.");
296
+ return 0;
297
+ }
298
+ default:
299
+ throw new CliError(
300
+ `Unknown org git subcommand: ${sub ?? "(none)"}. Try: status, connect, sync, disconnect`
301
+ );
302
+ }
303
+ }
@@ -89,6 +89,21 @@ const SEQUENCE_WINDOW_ARGS = {
89
89
  send_window_end_minute: { value: windowMinute("send-window-end") },
90
90
  send_window_timezone: "send-window-timezone",
91
91
  };
92
+ // --enroll-on-cake <stage>: start the sequence when a recipient's Daymaker cake
93
+ // reaches it; `none` clears it.
94
+ const CAKE_TRIGGER_STAGES = ["paid", "scheduled", "out_for_delivery", "delivered"];
95
+ const CAKE_STAGES = ["draft", ...CAKE_TRIGGER_STAGES, "delivery_failed", "cancelled"];
96
+ const enrollOnCake = {
97
+ value: (flags) => {
98
+ const raw = flagStr(flags["enroll-on-cake"]);
99
+ if (raw === undefined) return undefined;
100
+ if (raw === "none") return null;
101
+ if (!CAKE_TRIGGER_STAGES.includes(raw)) {
102
+ throw new CliError(`--enroll-on-cake takes ${CAKE_TRIGGER_STAGES.join(", ")} or none (got ${raw})`);
103
+ }
104
+ return raw;
105
+ },
106
+ };
92
107
  const PROSPECT_STATUSES = ["new", "contacted", "replied", "unsubscribed", "archived"];
93
108
  const mailerIds = {
94
109
  value: (flags) => {
@@ -147,6 +162,7 @@ export const ARGS = {
147
162
  mailer_ids: mailerIds,
148
163
  ...SEQUENCE_RAIL_ARGS,
149
164
  ...SEQUENCE_WINDOW_ARGS,
165
+ enroll_on_cake: enrollOnCake,
150
166
  },
151
167
  update_sequence: {
152
168
  sequence_id: { positional: 0, label: "sequence-id", required: true },
@@ -156,7 +172,15 @@ export const ARGS = {
156
172
  mailer_ids: mailerIds,
157
173
  ...SEQUENCE_RAIL_ARGS,
158
174
  ...SEQUENCE_WINDOW_ARGS,
175
+ enroll_on_cake: enrollOnCake,
159
176
  },
177
+ list_cakes: {
178
+ stage: { flag: "stage", enum: CAKE_STAGES },
179
+ limit: { flag: "limit", type: "num" },
180
+ },
181
+ sync_cakes: {},
182
+ connect_daymaker: { api_key: { flag: "daymaker-key", required: true } },
183
+ disconnect_daymaker: {},
160
184
  enroll_in_sequence: {
161
185
  sequence_id: { positional: 0, label: "sequence-id", required: true },
162
186
  recipients: { value: readRecipients },
@@ -357,7 +381,7 @@ async function run(ctx, tool, message) {
357
381
  return 0;
358
382
  }
359
383
 
360
- const SEQUENCES_USAGE = `Usage: sequences <list|get|create|update|pause|resume|enroll|stop|review|links>`;
384
+ const SEQUENCES_USAGE = `Usage: sequences <list|get|create|update|pause|resume|enroll|stop|review|links|cakes>`;
361
385
  const DISPATCHES_USAGE = `Usage: dispatches <list|create|rename|delete>`;
362
386
 
363
387
  // Dispatches: the named review desks a sequence's review stage queues to.
@@ -425,6 +449,19 @@ export async function cmdSequences(ctx) {
425
449
  return sequencesReview({ ...rest, positionals: rest.positionals.slice(1) }, rest.positionals[0]);
426
450
  case "links":
427
451
  return sequencesLinks({ ...rest, positionals: rest.positionals.slice(1) }, rest.positionals[0]);
452
+ case "cakes": {
453
+ const cakesCtx = { ...rest, positionals: rest.positionals.slice(1) };
454
+ switch (rest.positionals[0]) {
455
+ case "sync":
456
+ return run(cakesCtx, "sync_cakes", "Synced cakes from Daymaker.");
457
+ case "connect":
458
+ return run(cakesCtx, "connect_daymaker", "Daymaker connected.");
459
+ case "disconnect":
460
+ return run(cakesCtx, "disconnect_daymaker", "Daymaker disconnected.");
461
+ default:
462
+ return run(rest, "list_cakes");
463
+ }
464
+ }
428
465
  default:
429
466
  throw new CliError(`Unknown sequences subcommand: ${sub ?? "(none)"}. ${SEQUENCES_USAGE}`);
430
467
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.84.0",
3
+ "version": "0.85.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",