@letterstory/cli 0.100.2 → 0.100.5

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/lib/cli.mjs CHANGED
@@ -59,7 +59,7 @@ import {
59
59
  } from "./commands.mjs";
60
60
 
61
61
  // Keep in sync with cli/package.json.
62
- export const VERSION = "0.100.2";
62
+ export const VERSION = "0.100.5";
63
63
 
64
64
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
65
65
  // can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
@@ -77,6 +77,7 @@ export const BOOLEAN_FLAGS = new Set([
77
77
  "no-control",
78
78
  "own-beat",
79
79
  "refresh",
80
+ "preview",
80
81
  "accept",
81
82
  "help",
82
83
  "version",
@@ -1120,10 +1121,10 @@ Repo changes (Claude Code edits your repo in a sandbox; the Letterstory GitHub A
1120
1121
  repo repos Repos the Letterstory GitHub App can reach
1121
1122
  repo branches --repo owner/name Branches a change can start from (default first)
1122
1123
  repo start --repo <owner/name> --instructions <text>|--instructions-file <path|->
1123
- [--branch <base>] [--attach <path>]... [--attach-url <name>=<https url>]... [--wait]
1124
+ [--branch <base>] [--attach <path>]... [--attach-url <name>=<https url>]... [--preview] [--wait]
1124
1125
  Start a change (attached files go to the sandbox);
1125
1126
  --wait follows it to the PR
1126
- repo fix-audit --issue <issue-type> [--audit <id>] [--branch <base>] [--wait]
1127
+ repo fix-audit --issue <issue-type> [--audit <id>] [--branch <base>] [--preview] [--wait]
1127
1128
  One PR fixing a site-audit issue on every affected page,
1128
1129
  in the repo verified to build the site
1129
1130
  repo sites [--detect [--force]] Which website each repo builds, and the evidence
@@ -1132,7 +1133,8 @@ Repo changes (Claude Code edits your repo in a sandbox; the Letterstory GitHub A
1132
1133
  repo reset-site --repo <owner/name> --site <host> Back to automatic detection
1133
1134
  repo status <run-id> Phase, spend, PR link and Claude's summary
1134
1135
  repo list [--status running|succeeded|no_changes|failed|cancelled] [--limit n] [--audit <id>]
1135
- repo cancel <run-id> Stop it and delete the sandbox; no PR
1136
+ repo publish <run-id> Make PR on a preview in review
1137
+ repo cancel <run-id> Stop it and delete the sandbox; no PR (Discard a preview)
1136
1138
 
1137
1139
  Feedback (tell us what's broken, missing, unclear, or wanted):
1138
1140
  feedback submit --category bug|missing_feature|unclear|feature_request|praise|other
@@ -220,9 +220,17 @@ export function formatApply(result) {
220
220
  for (const a of result.skipped) lines.push(` ${SYMBOL[a.op]} ${a.section}/${a.name} — ${a.reason}`);
221
221
  if (result.failed.length) lines.push("Failed:");
222
222
  for (const a of result.failed) lines.push(` ${SYMBOL[a.op]} ${a.section}/${a.name} — ${a.error}`);
223
- if (result.gated?.length) {
223
+ // `gated` lists every gated change, allowed or not; after a real apply, show only those still held.
224
+ // A field-gated record can land in `applied` with its gated field skipped, so check `skipped` too.
225
+ const key = (a) => `${a.op} ${a.section}/${a.name}`;
226
+ const done = new Set(result.dryRun ? [] : result.applied.map(key));
227
+ const held = (result.gated ?? []).filter(
228
+ (a) =>
229
+ !done.has(key(a)) || result.skipped.some((s) => key(s) === key(a) && s.reason.includes(`gated (${a.gate})`))
230
+ );
231
+ if (held.length) {
224
232
  lines.push(`Gated (allow with --allow; confirm ${result.confirm}):`);
225
- for (const a of result.gated) lines.push(` ${SYMBOL[a.op]} ${a.section}/${a.name} — ${a.gate}`);
233
+ for (const a of held) lines.push(` ${SYMBOL[a.op]} ${a.section}/${a.name} — ${a.gate}`);
226
234
  }
227
235
  for (const w of result.plan.warnings) lines.push(`warning: ${w}`);
228
236
  return lines.join("\n");
@@ -2,6 +2,7 @@
2
2
  // pull request by the Letterstory GitHub App (src/lib/repo-changes). One subcommand per tool:
3
3
  // repo start → start_repo_change repo status → get_repo_change
4
4
  // repo list → list_repo_changes repo cancel → cancel_repo_change
5
+ // repo publish → publish_repo_change (Make PR on a preview)
5
6
  // repo repos → list_github_app_repos repo branches → list_repo_branches
6
7
 
7
8
  import { readFileSync } from "node:fs";
@@ -36,6 +37,7 @@ const ARGS = {
36
37
  instructions: { value: (flags) => readTextInput(flags, "instructions", "instructions-file") },
37
38
  // --attach <path> (uploaded first) and --attach-url <name>=<https url>, both repeatable.
38
39
  files: { value: (_flags, _positionals, extra) => (extra.files?.length ? extra.files : undefined) },
40
+ preview: { flag: "preview", type: "bool" },
39
41
  },
40
42
  create_repo_change_upload: { file_names: { internal: "the basenames of the --attach files" } },
41
43
  list_repo_sites: {},
@@ -55,9 +57,11 @@ const ARGS = {
55
57
  issue_type: { flag: "issue", required: true },
56
58
  audit_id: "audit",
57
59
  base_branch: "branch",
60
+ preview: { flag: "preview", type: "bool" },
58
61
  },
59
62
  get_repo_change: { run_id: { positional: 0, required: true, label: "run-id" } },
60
63
  cancel_repo_change: { run_id: { positional: 0, required: true, label: "run-id" } },
64
+ publish_repo_change: { run_id: { positional: 0, required: true, label: "run-id" } },
61
65
  list_repo_changes: {
62
66
  status: { flag: "status", enum: ["running", "succeeded", "no_changes", "failed", "cancelled"] },
63
67
  limit: { flag: "limit", type: "num" },
@@ -76,7 +80,10 @@ function describe(run) {
76
80
  const lines = [
77
81
  `${run.id} ${run.repo}@${run.baseBranch} ${run.status} (${run.phase}) $${run.costUsd.toFixed(2)}`,
78
82
  ];
79
- if (run.prUrl) lines.push(` PR: ${run.prUrl}`);
83
+ if (run.preview?.url) lines.push(` Preview: ${run.preview.url}`);
84
+ if (run.preview?.state === "failed") lines.push(` Preview failed: ${run.preview.detail ?? ""}`);
85
+ if (run.phase === "review") lines.push(` Make PR: letterstory repo publish ${run.id}`);
86
+ if (run.prUrl) lines.push(` PR: ${run.prUrl} (${run.prState ?? "state unknown"})`);
80
87
  if (run.errorCode) lines.push(` Error (${run.errorCode}): ${run.errorDetail ?? ""}`);
81
88
  if (run.summary) lines.push(...run.summary.split("\n").map((l) => ` ${l}`));
82
89
  return lines.join("\n");
@@ -102,13 +109,15 @@ export async function cmdRepo(ctx) {
102
109
  return repoList(rest);
103
110
  case "cancel":
104
111
  return repoCancel(rest);
112
+ case "publish":
113
+ return repoPublish(rest);
105
114
  case "repos":
106
115
  return repoRepos(rest);
107
116
  case "branches":
108
117
  return repoBranches(rest);
109
118
  default:
110
119
  throw new CliError(
111
- `Unknown repo subcommand: ${sub ?? "(none)"}. Try: start, fix-audit, sites, set-site, reset-site, status, list, cancel, repos, branches`
120
+ `Unknown repo subcommand: ${sub ?? "(none)"}. Try: start, fix-audit, sites, set-site, reset-site, status, list, publish, cancel, repos, branches`
112
121
  );
113
122
  }
114
123
  }
@@ -162,6 +171,7 @@ async function repoStart(ctx) {
162
171
  "instructions-file",
163
172
  "attach",
164
173
  "attach-url",
174
+ "preview",
165
175
  "wait",
166
176
  ]);
167
177
  const files = [
@@ -183,7 +193,7 @@ async function repoStart(ctx) {
183
193
  async () => {
184
194
  const polled = await client.callTool("get_repo_change", { run_id: started.run.id });
185
195
  ok(ctx, `… ${polled.run.status} (${polled.run.phase})`);
186
- return { done: TERMINAL.has(polled.run.status), value: polled.run };
196
+ return { done: settledFor(polled.run), value: polled.run };
187
197
  },
188
198
  { intervalMs: 30000, ceilingMs: 60 * 60 * 1000 }
189
199
  );
@@ -192,6 +202,11 @@ async function repoStart(ctx) {
192
202
  return value.status === "failed" ? 1 : 0;
193
203
  }
194
204
 
205
+ /** --wait stops at a terminal state, or at review: a preview waits on a person, not on time. */
206
+ function settledFor(run) {
207
+ return TERMINAL.has(run.status) || run.phase === "review";
208
+ }
209
+
195
210
  async function followRun(ctx, run) {
196
211
  const { client, flags, io } = ctx;
197
212
  const { settled, value } = await followJob(
@@ -199,7 +214,7 @@ async function followRun(ctx, run) {
199
214
  async () => {
200
215
  const polled = await client.callTool("get_repo_change", { run_id: run.id });
201
216
  ok(ctx, `… ${polled.run.status} (${polled.run.phase})`);
202
- return { done: TERMINAL.has(polled.run.status), value: polled.run };
217
+ return { done: settledFor(polled.run), value: polled.run };
203
218
  },
204
219
  { intervalMs: 30000, ceilingMs: 60 * 60 * 1000 }
205
220
  );
@@ -211,7 +226,7 @@ async function followRun(ctx, run) {
211
226
  /** `repo fix-audit --issue <type> [--audit <id>] [--wait]`: one audit issue → one PR, in the repo verified to build the site. */
212
227
  async function repoFixAudit(ctx) {
213
228
  const { client, flags, io } = ctx;
214
- refuseUnknownFlags(ctx, {}, ["issue", "audit", "branch", "wait"]);
229
+ refuseUnknownFlags(ctx, {}, ["issue", "audit", "branch", "preview", "wait"]);
215
230
  const result = await client.callTool(
216
231
  "fix_audit_issue",
217
232
  argsFromFlags(ARGS.fix_audit_issue, flags, ctx.positionals)
@@ -297,6 +312,18 @@ async function repoCancel(ctx) {
297
312
  return 0;
298
313
  }
299
314
 
315
+ /** `repo publish <run-id>`: Make PR on a preview in review. */
316
+ async function repoPublish(ctx) {
317
+ const { client, flags, io } = ctx;
318
+ const result = await client.callTool(
319
+ "publish_repo_change",
320
+ argsFromFlags(ARGS.publish_repo_change, flags, ctx.positionals)
321
+ );
322
+ ok(ctx, result.message);
323
+ printResult(io, flags, result.run, describe);
324
+ return 0;
325
+ }
326
+
300
327
  async function repoRepos(ctx) {
301
328
  const { client, flags, io } = ctx;
302
329
  const result = await client.callTool("list_github_app_repos", argsFromFlags(ARGS.list_github_app_repos, flags));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.100.2",
3
+ "version": "0.100.5",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",