@tryghost/ghst 0.5.0 → 0.7.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
@@ -1,4 +1,4 @@
1
- # ghst cli
1
+ # ghst cli (beta)
2
2
 
3
3
  `ghst` is a CLI tool for managing Ghost instances from the terminal. Anything you can do with the Ghost Admin API, you can do with `ghst`. (And a bit more)
4
4
 
@@ -109,10 +109,15 @@ Site/profile management:
109
109
  ghst auth list
110
110
  ghst auth switch <site-alias>
111
111
  ghst auth link --site <site-alias>
112
+ ghst auth link --site <site-alias> --yes
113
+ ghst auth logout --yes
112
114
  ghst auth token
113
115
  ```
114
116
 
115
117
  `ghst auth token` prints a short-lived staff JWT. Treat the output as sensitive.
118
+ `ghst auth logout` requires confirmation when removing all configured sites; use `--yes` in non-interactive scripts.
119
+ `ghst auth link` requires confirmation before replacing an existing project link; use `--yes` in non-interactive scripts.
120
+ Interactive destructive confirmations also emit `GHST_AGENT_NOTICE:` lines on stderr instructing cooperative agents to ask the user for approval before continuing.
116
121
 
117
122
  ## Command Reference
118
123
 
@@ -140,7 +145,7 @@ ghst auth token
140
145
  | `config` | `show`, `path`, `list`, `get`, `set` |
141
146
  | `api` | raw Ghost request command (`ghst api [endpointPath]`) |
142
147
  | `mcp` | `stdio`, `http` |
143
- | `completion` | `<bash|zsh|fish|powershell>` |
148
+ | `completion` | `bash`, `zsh`, `fish`, or `powershell` |
144
149
 
145
150
  ## Global Options
146
151
 
@@ -229,6 +234,7 @@ ghst socialweb status
229
234
  ghst socialweb profile
230
235
  ghst socialweb notes --json
231
236
  ghst socialweb follow @alice@example.com
237
+ ghst socialweb delete https://example.com/.ghost/activitypub/note/1 --yes
232
238
  ghst socialweb note --content "Hello fediverse"
233
239
  ghst socialweb reply https://example.com/users/alice/statuses/1 --content "Replying from ghst"
234
240
  ```
@@ -236,6 +242,7 @@ ghst socialweb reply https://example.com/users/alice/statuses/1 --content "Reply
236
242
  Social web auth note:
237
243
  - `ghst socialweb` bootstraps a short-lived identity JWT from `/ghost/api/admin/identities/`.
238
244
  - That bridge requires an Owner or Administrator staff access token.
245
+ - `ghst socialweb delete` requires confirmation; use `--yes` in non-interactive scripts.
239
246
  - Public Ghost post publishing still lives under `ghst post`; `ghst socialweb` is for notes, interactions, profile, feed, and moderation flows.
240
247
 
241
248
  Ghost analytics filter semantics:
@@ -246,6 +253,9 @@ Ghost range semantics:
246
253
  - `stats growth` clips member, MRR, and subscription histories client-side when Ghost only exposes broader source data.
247
254
  - `stats post ... growth` clips Ghost's lifetime post-growth history to the selected window.
248
255
 
256
+ File output safety:
257
+ - `ghst member export --output`, `ghst stats ... --csv --output`, and `ghst migrate export --output` refuse to overwrite an existing file.
258
+
249
259
  `endpointPath` must stay within the selected Ghost API root. Use resource paths such as `/posts/`
250
260
  or canonical Ghost API paths such as `/ghost/api/admin/posts/`.
251
261
 
@@ -344,9 +354,9 @@ The `stats` MCP tools mirror the CLI analytics surface, including `ghst stats ov
344
354
  and `ghst stats post <post-id> referrers`. The same Ghost analytics filter and range semantics
345
355
  shown above apply to both the CLI and MCP stats tooling.
346
356
 
347
- The `socialweb` MCP tools mirror the private `ghst socialweb` admin surface for status,
348
- profile, feeds, interactions, moderation, and uploads. They use the same Owner/Admin
349
- identity-token bridge as the CLI and remain limited to Ghost's private social web admin APIs.
357
+ The `socialweb` MCP tools mirror the `ghst socialweb` CLI surface for status, profile,
358
+ feeds, interactions, moderation, and uploads. They use the same Owner/Admin auth flow as
359
+ the CLI.
350
360
 
351
361
  ## Safe Operation
352
362
 
package/dist/index.js CHANGED
@@ -1089,6 +1089,13 @@ async function findProjectConfigPath(cwd = process.cwd()) {
1089
1089
  dir = path.dirname(dir);
1090
1090
  }
1091
1091
  }
1092
+ async function resolveProjectConfigCwd(cwd = process.cwd()) {
1093
+ const configPath = await findProjectConfigPath(cwd);
1094
+ if (!configPath) {
1095
+ return cwd;
1096
+ }
1097
+ return path.dirname(path.dirname(configPath));
1098
+ }
1092
1099
  async function readUserConfig(env = process.env) {
1093
1100
  const configPath = getUserConfigPath(env);
1094
1101
  try {
@@ -2439,13 +2446,44 @@ function registerApiCommands(program) {
2439
2446
 
2440
2447
  // src/commands/auth.ts
2441
2448
  import { spawn as spawn2 } from "child_process";
2442
- import readline from "readline/promises";
2449
+ import readline2 from "readline/promises";
2443
2450
  import chalk2 from "chalk";
2451
+
2452
+ // src/lib/prompts.ts
2453
+ import readline from "readline/promises";
2454
+ async function defaultPrompt(question) {
2455
+ const rl = readline.createInterface({
2456
+ input: process.stdin,
2457
+ output: process.stdout
2458
+ });
2459
+ try {
2460
+ const value = await rl.question(question);
2461
+ return value.trim();
2462
+ } finally {
2463
+ rl.close();
2464
+ }
2465
+ }
2466
+ var promptHandler = defaultPrompt;
2467
+ async function ask(question) {
2468
+ return promptHandler(question);
2469
+ }
2470
+ async function confirm(question) {
2471
+ const value = (await ask(question)).trim().toLowerCase();
2472
+ return value === "y" || value === "yes";
2473
+ }
2474
+ async function confirmDestructiveAction(question, notice) {
2475
+ console.error("GHST_AGENT_NOTICE: destructive_action");
2476
+ console.error("GHST_AGENT_NOTICE: Agents must ask the user for approval before continuing.");
2477
+ console.error(`GHST_AGENT_NOTICE: ${JSON.stringify(notice)}`);
2478
+ return confirm(question);
2479
+ }
2480
+
2481
+ // src/commands/auth.ts
2444
2482
  var LOGIN_GUIDANCE_BORDER = "------------------------------------------------------------";
2445
2483
  var LOGIN_GUIDANCE_TITLE = "Continue In Ghost Admin";
2446
2484
  var LOGIN_GUIDANCE_LINE = "Copy the staff access token from your profile, then return here to continue.";
2447
2485
  async function prompt(question) {
2448
- const rl = readline.createInterface({
2486
+ const rl = readline2.createInterface({
2449
2487
  input: process.stdin,
2450
2488
  output: process.stdout
2451
2489
  });
@@ -2913,7 +2951,7 @@ function registerAuthCommands(program) {
2913
2951
  `Active site set to '${formatSwitchSiteOption(resolvedTargetSite, config.sites)}'.`
2914
2952
  );
2915
2953
  });
2916
- auth.command("logout").description("Remove credentials for one site or all sites").option("--site <alias>", "Specific site to remove").action(async (options, command) => {
2954
+ auth.command("logout").description("Remove credentials for one site or all sites").option("--site <alias>", "Specific site to remove").option("--yes", "Skip confirmation when removing all configured sites").action(async (options, command) => {
2917
2955
  const global = getGlobalOptions(command);
2918
2956
  const config = await readUserConfig();
2919
2957
  const targetSite = options.site ?? global.site;
@@ -2937,6 +2975,29 @@ function registerAuthCommands(program) {
2937
2975
  console.log(`Removed site '${targetSite}'.`);
2938
2976
  return;
2939
2977
  }
2978
+ if (!options.yes) {
2979
+ if (isNonInteractive()) {
2980
+ throw new GhstError("Removing all sites in non-interactive mode requires --yes.", {
2981
+ exitCode: 2 /* USAGE_ERROR */,
2982
+ code: "USAGE_ERROR"
2983
+ });
2984
+ }
2985
+ const ok = await confirmDestructiveAction(
2986
+ "Remove all configured sites and credentials? [y/N]: ",
2987
+ {
2988
+ action: "logout_all_sites",
2989
+ target: "all_configured_sites",
2990
+ reversible: false,
2991
+ sideEffects: ["remove_credentials", "remove_site_links"]
2992
+ }
2993
+ );
2994
+ if (!ok) {
2995
+ throw new GhstError("Operation cancelled.", {
2996
+ exitCode: 4 /* OPERATION_CANCELLED */,
2997
+ code: "OPERATION_CANCELLED"
2998
+ });
2999
+ }
3000
+ }
2940
3001
  for (const site of Object.values(config.sites)) {
2941
3002
  if (site.credentialRef) {
2942
3003
  await store.delete(site.credentialRef).catch(() => void 0);
@@ -2947,7 +3008,7 @@ function registerAuthCommands(program) {
2947
3008
  await writeUserConfig(config);
2948
3009
  console.log("Removed all configured sites.");
2949
3010
  });
2950
- auth.command("link").description("Link current project directory to a configured site alias").option("--site <alias>", "Site alias to link").action(async (options, command) => {
3011
+ auth.command("link").description("Link current project directory to a configured site alias").option("--site <alias>", "Site alias to link").option("--yes", "Skip confirmation when replacing an existing project link").action(async (options, command) => {
2951
3012
  const global = getGlobalOptions(command);
2952
3013
  const config = await readUserConfig();
2953
3014
  const siteAlias = options.site ?? global.site ?? config.active;
@@ -2957,9 +3018,44 @@ function registerAuthCommands(program) {
2957
3018
  code: "SITE_REQUIRED"
2958
3019
  });
2959
3020
  }
2960
- await writeProjectConfig({
2961
- site: siteAlias
2962
- });
3021
+ const projectConfig = await readProjectConfig();
3022
+ const projectConfigCwd = await resolveProjectConfigCwd();
3023
+ if (projectConfig && projectConfig.site !== siteAlias) {
3024
+ if (!options.yes) {
3025
+ if (isNonInteractive()) {
3026
+ throw new GhstError(
3027
+ "Overwriting an existing project link in non-interactive mode requires --yes.",
3028
+ {
3029
+ exitCode: 2 /* USAGE_ERROR */,
3030
+ code: "USAGE_ERROR"
3031
+ }
3032
+ );
3033
+ }
3034
+ const ok = await confirmDestructiveAction(
3035
+ `Relink current directory from '${projectConfig.site}' to '${siteAlias}'? [y/N]: `,
3036
+ {
3037
+ action: "relink_project",
3038
+ target: `${projectConfig.site}->${siteAlias}`,
3039
+ reversible: true,
3040
+ site: siteAlias,
3041
+ sideEffects: ["update_project_link"]
3042
+ }
3043
+ );
3044
+ if (!ok) {
3045
+ throw new GhstError("Operation cancelled.", {
3046
+ exitCode: 4 /* OPERATION_CANCELLED */,
3047
+ code: "OPERATION_CANCELLED"
3048
+ });
3049
+ }
3050
+ }
3051
+ }
3052
+ await writeProjectConfig(
3053
+ {
3054
+ ...projectConfig?.defaults ? { defaults: projectConfig.defaults } : {},
3055
+ site: siteAlias
3056
+ },
3057
+ projectConfigCwd
3058
+ );
2963
3059
  console.log(`Linked current directory to '${siteAlias}'.`);
2964
3060
  });
2965
3061
  auth.command("token").description("Print a short-lived staff JWT for the active connection (sensitive output)").action(async (_, command) => {
@@ -3099,29 +3195,6 @@ async function setCommentStatus(global, id, status) {
3099
3195
  return client.comments.setStatus(id, status);
3100
3196
  }
3101
3197
 
3102
- // src/lib/prompts.ts
3103
- import readline2 from "readline/promises";
3104
- async function defaultPrompt(question) {
3105
- const rl = readline2.createInterface({
3106
- input: process.stdin,
3107
- output: process.stdout
3108
- });
3109
- try {
3110
- const value = await rl.question(question);
3111
- return value.trim();
3112
- } finally {
3113
- rl.close();
3114
- }
3115
- }
3116
- var promptHandler = defaultPrompt;
3117
- async function ask(question) {
3118
- return promptHandler(question);
3119
- }
3120
- async function confirm(question) {
3121
- const value = (await ask(question)).trim().toLowerCase();
3122
- return value === "y" || value === "yes";
3123
- }
3124
-
3125
3198
  // src/schemas/comment.ts
3126
3199
  import { z as z3 } from "zod";
3127
3200
  var CommentListLimitSchema = z3.union([z3.number().int().positive().max(100), z3.literal("all")]);
@@ -3365,7 +3438,12 @@ function registerCommentCommands(program) {
3365
3438
  exitCode: 2 /* USAGE_ERROR */
3366
3439
  });
3367
3440
  }
3368
- const ok = await confirm(`Delete comment '${parsed.data.id}'? [y/N]: `);
3441
+ const ok = await confirmDestructiveAction(`Delete comment '${parsed.data.id}'? [y/N]: `, {
3442
+ action: "delete_comment",
3443
+ target: parsed.data.id,
3444
+ reversible: false,
3445
+ site: global.site ?? null
3446
+ });
3369
3447
  if (!ok) {
3370
3448
  throw new GhstError("Operation cancelled.", {
3371
3449
  code: "OPERATION_CANCELLED",
@@ -3991,7 +4069,12 @@ function registerLabelCommands(program) {
3991
4069
  exitCode: 2 /* USAGE_ERROR */
3992
4070
  });
3993
4071
  }
3994
- const ok = await confirm(`Delete label '${parsed.data.id}'? [y/N]: `);
4072
+ const ok = await confirmDestructiveAction(`Delete label '${parsed.data.id}'? [y/N]: `, {
4073
+ action: "delete_label",
4074
+ target: parsed.data.id,
4075
+ reversible: false,
4076
+ site: global.site ?? null
4077
+ });
3995
4078
  if (!ok) {
3996
4079
  throw new GhstError("Operation cancelled.", {
3997
4080
  code: "OPERATION_CANCELLED",
@@ -8720,7 +8803,24 @@ function registerMcpCommands(program) {
8720
8803
  }
8721
8804
 
8722
8805
  // src/commands/member.ts
8806
+ import fs8 from "fs/promises";
8807
+
8808
+ // src/lib/file-guards.ts
8723
8809
  import fs7 from "fs/promises";
8810
+ async function assertFileDoesNotExist(filePath) {
8811
+ try {
8812
+ await fs7.access(filePath);
8813
+ } catch (error) {
8814
+ if (error.code === "ENOENT") {
8815
+ return;
8816
+ }
8817
+ throw error;
8818
+ }
8819
+ throw new GhstError(`Refusing to overwrite existing file: ${filePath}`, {
8820
+ code: "USAGE_ERROR",
8821
+ exitCode: 2 /* USAGE_ERROR */
8822
+ });
8823
+ }
8724
8824
 
8725
8825
  // src/schemas/member.ts
8726
8826
  import { z as z7 } from "zod";
@@ -9040,7 +9140,13 @@ function registerMemberCommands(program) {
9040
9140
  exitCode: 2 /* USAGE_ERROR */
9041
9141
  });
9042
9142
  }
9043
- const ok = await confirm(`Delete member '${parsed.data.id}'? [y/N]: `);
9143
+ const ok = await confirmDestructiveAction(`Delete member '${parsed.data.id}'? [y/N]: `, {
9144
+ action: "delete_member",
9145
+ target: parsed.data.id,
9146
+ reversible: false,
9147
+ site: global.site ?? null,
9148
+ sideEffects: parsed.data.cancel ? ["cancel_subscriptions"] : void 0
9149
+ });
9044
9150
  if (!ok) {
9045
9151
  throw new GhstError("Operation cancelled.", {
9046
9152
  code: "OPERATION_CANCELLED",
@@ -9094,7 +9200,8 @@ function registerMemberCommands(program) {
9094
9200
  search: parsed.data.search
9095
9201
  });
9096
9202
  if (parsed.data.output) {
9097
- await fs7.writeFile(parsed.data.output, csv, "utf8");
9203
+ await assertFileDoesNotExist(parsed.data.output);
9204
+ await fs8.writeFile(parsed.data.output, csv, "utf8");
9098
9205
  if (global.json) {
9099
9206
  printJson({ ok: true, output: parsed.data.output });
9100
9207
  return;
@@ -9145,7 +9252,7 @@ function registerMemberCommands(program) {
9145
9252
  }
9146
9253
 
9147
9254
  // src/lib/migrate.ts
9148
- import fs8 from "fs/promises";
9255
+ import fs9 from "fs/promises";
9149
9256
  import os2 from "os";
9150
9257
  import path6 from "path";
9151
9258
  import { slugify } from "@tryghost/string";
@@ -9352,7 +9459,7 @@ function parseCsvRows(raw) {
9352
9459
  }
9353
9460
  async function uploadDbImportFile(global, filePath) {
9354
9461
  const client = await getClient17(global);
9355
- const bytes = await fs8.readFile(filePath);
9462
+ const bytes = await fs9.readFile(filePath);
9356
9463
  const formData = new FormData();
9357
9464
  formData.append("importfile", new Blob([bytes]), path6.basename(filePath));
9358
9465
  return client.db.import(formData);
@@ -9368,9 +9475,9 @@ async function buildGhostDbImportFile(input) {
9368
9475
  }
9369
9476
  const ghostJson = await toGhostJSON(input, {});
9370
9477
  const payload = { db: [ghostJson] };
9371
- const tempDir = await fs8.mkdtemp(path6.join(os2.tmpdir(), "ghst-import-json-"));
9478
+ const tempDir = await fs9.mkdtemp(path6.join(os2.tmpdir(), "ghst-import-json-"));
9372
9479
  const outputPath = path6.join(tempDir, "import.json");
9373
- await fs8.writeFile(outputPath, JSON.stringify(payload), "utf8");
9480
+ await fs9.writeFile(outputPath, JSON.stringify(payload), "utf8");
9374
9481
  return outputPath;
9375
9482
  }
9376
9483
  async function migrateImportJson(global, filePath) {
@@ -9485,7 +9592,7 @@ async function migrateImportSubstack(global, filePath, url) {
9485
9592
  return uploadDbImportFile(global, importPath);
9486
9593
  }
9487
9594
  async function migrateImportCsv(global, filePath) {
9488
- const raw = await fs8.readFile(filePath, "utf8");
9595
+ const raw = await fs9.readFile(filePath, "utf8");
9489
9596
  const rows = parseCsvRows(raw);
9490
9597
  const posts = rows.map((row, index) => {
9491
9598
  const html = row.html ?? markdownRenderer.render(row.markdown ?? "");
@@ -9523,7 +9630,8 @@ async function migrateImportCsv(global, filePath) {
9523
9630
  async function migrateExport(global, outputPath) {
9524
9631
  const client = await getClient17(global);
9525
9632
  const data = await client.db.export();
9526
- await fs8.writeFile(outputPath, data);
9633
+ await assertFileDoesNotExist(outputPath);
9634
+ await fs9.writeFile(outputPath, data);
9527
9635
  return outputPath;
9528
9636
  }
9529
9637
 
@@ -10133,7 +10241,7 @@ function registerOfferCommands(program) {
10133
10241
  }
10134
10242
 
10135
10243
  // src/commands/page.ts
10136
- import fs9 from "fs/promises";
10244
+ import fs10 from "fs/promises";
10137
10245
 
10138
10246
  // src/schemas/page.ts
10139
10247
  import { z as z11 } from "zod";
@@ -10248,7 +10356,7 @@ async function readOptionalFile(filePath) {
10248
10356
  if (!filePath) {
10249
10357
  return void 0;
10250
10358
  }
10251
- return fs9.readFile(filePath, "utf8");
10359
+ return fs10.readFile(filePath, "utf8");
10252
10360
  }
10253
10361
  function registerPageCommands(program) {
10254
10362
  const page = program.command("page").description("Page management");
@@ -10415,7 +10523,12 @@ function registerPageCommands(program) {
10415
10523
  exitCode: 2 /* USAGE_ERROR */
10416
10524
  });
10417
10525
  }
10418
- const ok = await confirm(`Delete page '${parsed.data.id}'? [y/N]: `);
10526
+ const ok = await confirmDestructiveAction(`Delete page '${parsed.data.id}'? [y/N]: `, {
10527
+ action: "delete_page",
10528
+ target: parsed.data.id,
10529
+ reversible: false,
10530
+ site: global.site ?? null
10531
+ });
10419
10532
  if (!ok) {
10420
10533
  throw new GhstError("Operation cancelled.", {
10421
10534
  code: "OPERATION_CANCELLED",
@@ -10471,7 +10584,7 @@ function registerPageCommands(program) {
10471
10584
  }
10472
10585
 
10473
10586
  // src/commands/post.ts
10474
- import fs10 from "fs/promises";
10587
+ import fs11 from "fs/promises";
10475
10588
  import MarkdownIt2 from "markdown-it";
10476
10589
 
10477
10590
  // src/schemas/post.ts
@@ -10685,7 +10798,7 @@ async function readOptionalFile2(filePath) {
10685
10798
  if (!filePath) {
10686
10799
  return void 0;
10687
10800
  }
10688
- return fs10.readFile(filePath, "utf8");
10801
+ return fs11.readFile(filePath, "utf8");
10689
10802
  }
10690
10803
  async function readOptionalStdin(enabled) {
10691
10804
  if (!enabled) {
@@ -10723,7 +10836,7 @@ async function readOptionalPostJson(filePath) {
10723
10836
  if (!filePath) {
10724
10837
  return {};
10725
10838
  }
10726
- const payload = JSON.parse(await fs10.readFile(filePath, "utf8"));
10839
+ const payload = JSON.parse(await fs11.readFile(filePath, "utf8"));
10727
10840
  return asPostPayload(payload);
10728
10841
  }
10729
10842
  function assignDefined(target, values) {
@@ -10979,7 +11092,13 @@ function registerPostCommands(program) {
10979
11092
  });
10980
11093
  }
10981
11094
  const label = parsed.data.filter ? `Delete posts matching '${parsed.data.filter}'` : `Delete post '${parsed.data.id}'`;
10982
- const ok = await confirm(`${label}? [y/N]: `);
11095
+ const ok = await confirmDestructiveAction(`${label}? [y/N]: `, {
11096
+ action: "delete_post",
11097
+ target: parsed.data.filter ?? parsed.data.id ?? "(unknown)",
11098
+ count: parsed.data.filter ? void 0 : 1,
11099
+ reversible: false,
11100
+ site: global.site ?? null
11101
+ });
10983
11102
  if (!ok) {
10984
11103
  throw new GhstError("Operation cancelled.", {
10985
11104
  code: "OPERATION_CANCELLED",
@@ -11226,7 +11345,7 @@ function registerSiteCommands(program) {
11226
11345
  }
11227
11346
 
11228
11347
  // src/commands/socialweb.ts
11229
- import fs11 from "fs/promises";
11348
+ import fs12 from "fs/promises";
11230
11349
 
11231
11350
  // src/schemas/socialweb.ts
11232
11351
  import { z as z14 } from "zod";
@@ -11292,6 +11411,10 @@ var SocialWebHandleActionInputSchema = z14.object({
11292
11411
  var SocialWebIdInputSchema = z14.object({
11293
11412
  id: UrlSchema2
11294
11413
  });
11414
+ var SocialWebDeleteInputSchema = z14.object({
11415
+ id: UrlSchema2,
11416
+ yes: z14.boolean().optional()
11417
+ });
11295
11418
  var SocialWebBlockDomainInputSchema = z14.object({
11296
11419
  url: UrlSchema2
11297
11420
  });
@@ -11580,10 +11703,9 @@ function registerSocialWebCommands(program) {
11580
11703
  ["like", likePost, "Like a post"],
11581
11704
  ["unlike", unlikePost, "Unlike a post"],
11582
11705
  ["repost", repostPost, "Repost a post"],
11583
- ["derepost", derepostPost, "Undo repost on a post"],
11584
- ["delete", deleteSocialWebPost, "Delete a post"]
11706
+ ["derepost", derepostPost, "Undo repost on a post"]
11585
11707
  ]) {
11586
- socialweb.command(`${entry[0]} <id>`).description(entry[2]).action(async (id, _, command) => {
11708
+ socialweb.command(`${entry[0]} <id>`).description(entry[2]).action(async (id, _options, command) => {
11587
11709
  const global = getGlobalOptions(command);
11588
11710
  const parsed = SocialWebIdInputSchema.safeParse({ id });
11589
11711
  if (!parsed.success) {
@@ -11594,9 +11716,48 @@ function registerSocialWebCommands(program) {
11594
11716
  printJson(payload, global.jq);
11595
11717
  return;
11596
11718
  }
11597
- console.log(entry[0] === "delete" ? "Deleted post" : "OK");
11719
+ console.log("OK");
11598
11720
  });
11599
11721
  }
11722
+ socialweb.command("delete <id>").description("Delete a post").option("--yes", "Skip confirmation").action(async (id, options, command) => {
11723
+ const global = getGlobalOptions(command);
11724
+ const parsed = SocialWebDeleteInputSchema.safeParse({
11725
+ id,
11726
+ yes: parseBooleanFlag(options.yes)
11727
+ });
11728
+ if (!parsed.success) {
11729
+ throwValidationError11(parsed.error);
11730
+ }
11731
+ if (!parsed.data.yes) {
11732
+ if (isNonInteractive()) {
11733
+ throw new GhstError("Deleting in non-interactive mode requires --yes.", {
11734
+ code: "USAGE_ERROR",
11735
+ exitCode: 2 /* USAGE_ERROR */
11736
+ });
11737
+ }
11738
+ const ok = await confirmDestructiveAction(
11739
+ `Delete social web post '${parsed.data.id}'? [y/N]: `,
11740
+ {
11741
+ action: "delete_socialweb_post",
11742
+ target: parsed.data.id,
11743
+ reversible: false,
11744
+ site: global.site ?? null
11745
+ }
11746
+ );
11747
+ if (!ok) {
11748
+ throw new GhstError("Operation cancelled.", {
11749
+ code: "OPERATION_CANCELLED",
11750
+ exitCode: 4 /* OPERATION_CANCELLED */
11751
+ });
11752
+ }
11753
+ }
11754
+ const payload = await deleteSocialWebPost(global, parsed.data.id);
11755
+ if (global.json) {
11756
+ printJson(payload, global.jq);
11757
+ return;
11758
+ }
11759
+ console.log("Deleted post");
11760
+ });
11600
11761
  socialweb.command("note").description("Create a social web note").option("--content <text>", "Note content").option("--stdin", "Read note content from stdin").option("--image-file <path>", "Attach an image from a local file").option("--image-url <url>", "Attach an image by URL").option("--image-alt <text>", "Image alt text").action(async (options, command) => {
11601
11762
  const global = getGlobalOptions(command);
11602
11763
  const parsed = SocialWebContentInputSchema.safeParse({
@@ -11733,7 +11894,7 @@ function registerSocialWebCommands(program) {
11733
11894
  throwValidationError11(parsed.error);
11734
11895
  }
11735
11896
  try {
11736
- await fs11.access(parsed.data.filePath);
11897
+ await fs12.access(parsed.data.filePath);
11737
11898
  } catch {
11738
11899
  throw new GhstError(`File not found: ${parsed.data.filePath}`, {
11739
11900
  code: "VALIDATION_ERROR",
@@ -11750,7 +11911,7 @@ function registerSocialWebCommands(program) {
11750
11911
  }
11751
11912
 
11752
11913
  // src/commands/stats.ts
11753
- import fs12 from "fs/promises";
11914
+ import fs13 from "fs/promises";
11754
11915
 
11755
11916
  // src/schemas/stats.ts
11756
11917
  import { z as z15 } from "zod";
@@ -12079,7 +12240,8 @@ function postReferrersCsv(payload) {
12079
12240
  }
12080
12241
  async function emitCsv(csv, output) {
12081
12242
  if (output) {
12082
- await fs12.writeFile(output, `${csv}
12243
+ await assertFileDoesNotExist(output);
12244
+ await fs13.writeFile(output, `${csv}
12083
12245
  `, "utf8");
12084
12246
  return;
12085
12247
  }
@@ -12598,7 +12760,12 @@ function registerTagCommands(program) {
12598
12760
  exitCode: 2 /* USAGE_ERROR */
12599
12761
  });
12600
12762
  }
12601
- const ok = await confirm(`Delete tag '${parsed.data.id}'? [y/N]: `);
12763
+ const ok = await confirmDestructiveAction(`Delete tag '${parsed.data.id}'? [y/N]: `, {
12764
+ action: "delete_tag",
12765
+ target: parsed.data.id,
12766
+ reversible: false,
12767
+ site: global.site ?? null
12768
+ });
12602
12769
  if (!ok) {
12603
12770
  throw new GhstError("Operation cancelled.", {
12604
12771
  code: "OPERATION_CANCELLED",
@@ -12642,13 +12809,13 @@ function registerTagCommands(program) {
12642
12809
 
12643
12810
  // src/commands/theme.ts
12644
12811
  import { spawn as spawn4 } from "child_process";
12645
- import fs14 from "fs/promises";
12812
+ import fs15 from "fs/promises";
12646
12813
  import os4 from "os";
12647
12814
  import path8 from "path";
12648
12815
 
12649
12816
  // src/lib/theme-dev.ts
12650
12817
  import { spawn as spawn3 } from "child_process";
12651
- import fs13 from "fs";
12818
+ import fs14 from "fs";
12652
12819
  import fsPromises from "fs/promises";
12653
12820
  import os3 from "os";
12654
12821
  import path7 from "path";
@@ -12705,7 +12872,7 @@ async function runThemeDev(global, options) {
12705
12872
  if (!options.watch) {
12706
12873
  return initial.payload;
12707
12874
  }
12708
- const watcher = fs13.watch(options.path, { recursive: true });
12875
+ const watcher = fs14.watch(options.path, { recursive: true });
12709
12876
  let timer = null;
12710
12877
  let running = false;
12711
12878
  let queued = false;
@@ -12817,7 +12984,7 @@ function throwValidationError14(error) {
12817
12984
  );
12818
12985
  }
12819
12986
  async function zipDirectory2(directoryPath) {
12820
- const tempDir = await fs14.mkdtemp(path8.join(os4.tmpdir(), "ghst-theme-"));
12987
+ const tempDir = await fs15.mkdtemp(path8.join(os4.tmpdir(), "ghst-theme-"));
12821
12988
  const zipPath = path8.join(tempDir, `${path8.basename(directoryPath)}.zip`);
12822
12989
  await new Promise((resolve, reject) => {
12823
12990
  const child = spawn4("zip", ["-r", zipPath, "."], {
@@ -12838,7 +13005,7 @@ async function zipDirectory2(directoryPath) {
12838
13005
  async function validateTheme(targetPath) {
12839
13006
  const gscanModule = await import("gscan");
12840
13007
  const gscan = gscanModule.default ?? gscanModule;
12841
- const stat = await fs14.stat(targetPath);
13008
+ const stat = await fs15.stat(targetPath);
12842
13009
  if (stat.isDirectory()) {
12843
13010
  const result2 = await gscan.check?.(targetPath, {
12844
13011
  checkVersion: "v6"
@@ -12894,7 +13061,7 @@ function registerThemeCommands(program) {
12894
13061
  if (!parsed.success) {
12895
13062
  throwValidationError14(parsed.error);
12896
13063
  }
12897
- const stat = await fs14.stat(parsed.data.path);
13064
+ const stat = await fs15.stat(parsed.data.path);
12898
13065
  let uploadPath = parsed.data.path;
12899
13066
  if (stat.isDirectory()) {
12900
13067
  if (!parsed.data.zip) {
@@ -13795,7 +13962,12 @@ function registerWebhookCommands(program) {
13795
13962
  exitCode: 2 /* USAGE_ERROR */
13796
13963
  });
13797
13964
  }
13798
- const ok = await confirm(`Delete webhook '${parsed.data.id}'? [y/N]: `);
13965
+ const ok = await confirmDestructiveAction(`Delete webhook '${parsed.data.id}'? [y/N]: `, {
13966
+ action: "delete_webhook",
13967
+ target: parsed.data.id,
13968
+ reversible: false,
13969
+ site: global.site ?? null
13970
+ });
13799
13971
  if (!ok) {
13800
13972
  throw new GhstError("Operation cancelled.", {
13801
13973
  code: "OPERATION_CANCELLED",