gh-postplan 0.1.1 → 0.2.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
@@ -45,6 +45,15 @@ npx gh-postplan publish ./draft.html --no-wait
45
45
 
46
46
  Only the HTML file is uploaded. Use self-contained HTML or absolute URLs for assets.
47
47
 
48
+ ## List and delete
49
+
50
+ ```sh
51
+ npx gh-postplan list
52
+ npx gh-postplan delete
53
+ ```
54
+
55
+ `delete` shows the published drafts and accepts multiple comma-separated choices. Deleting a draft removes its stable URL and every permanent version.
56
+
48
57
  Configuration lives in `~/.config/gh-postplan`. Set `GH_POSTPLAN_REPO=owner/repo` to override the configured repository.
49
58
 
50
59
  The npm package also includes an agent skill at `skills/gh-postplan/SKILL.md`.
package/dist/cli.js CHANGED
@@ -16,8 +16,9 @@ import {
16
16
  } from "node:fs/promises";
17
17
  import { homedir } from "node:os";
18
18
  import { dirname, extname, join, resolve } from "node:path";
19
+ import { createInterface } from "node:readline/promises";
19
20
  import { fileURLToPath } from "node:url";
20
- var packageVersion = "0.1.0";
21
+ var packageVersion = "0.2.0";
21
22
  var configDirectory = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "gh-postplan");
22
23
  var cacheDirectory = join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "gh-postplan");
23
24
  var configPath = join(configDirectory, "config.json");
@@ -28,10 +29,14 @@ class PushError extends Error {
28
29
  var help = `Usage:
29
30
  gh-postplan setup OWNER/REPO [--create]
30
31
  gh-postplan publish FILE [--new | --draft ID] [--no-wait]
32
+ gh-postplan list
33
+ gh-postplan delete
31
34
 
32
35
  Commands:
33
36
  setup Configure an existing repo, or create a public one with --create
34
- publish Publish one HTML file and keep its older versions available`;
37
+ publish Publish one HTML file and keep its older versions available
38
+ list List published drafts
39
+ delete Interactively delete one or more drafts and all their versions`;
35
40
  function parseCommand(args) {
36
41
  if (args.length === 0 || args.includes("--help") || args.includes("-h"))
37
42
  return { kind: "help" };
@@ -70,6 +75,8 @@ ${help}`);
70
75
  throw new Error("Invalid draft ID");
71
76
  return { kind: "publish", file: subject, draft, fresh, wait };
72
77
  }
78
+ if ((name === "list" || name === "delete") && !subject && flags.length === 0)
79
+ return { kind: name };
73
80
  throw new Error(`Unknown command: ${name ?? ""}
74
81
 
75
82
  ${help}`);
@@ -81,6 +88,15 @@ function pageUrls(base, id, version) {
81
88
  const root = `${base.replace(/\/+$/, "")}/drafts/${id}/`;
82
89
  return { current: root, version: `${root}v${version}/` };
83
90
  }
91
+ function parseSelection(answer, count) {
92
+ if (!answer.trim())
93
+ return [];
94
+ const choices = answer.split(",").map((choice) => Number(choice.trim()));
95
+ if (choices.some((choice) => !Number.isInteger(choice) || choice < 1 || choice > count)) {
96
+ throw new Error(`Choose numbers from 1 to ${count}, separated by commas`);
97
+ }
98
+ return [...new Set(choices)];
99
+ }
84
100
  function validateRepo(repo) {
85
101
  if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) {
86
102
  throw new Error("Repository must look like OWNER/REPO");
@@ -254,6 +270,68 @@ async function getRepo() {
254
270
  validateRepo(repo);
255
271
  return repo;
256
272
  }
273
+ async function readPublishedDrafts(repo) {
274
+ const path = await syncClone(repo);
275
+ const pages = await readPages(repo);
276
+ const localDrafts = await readJson(draftsPath, {});
277
+ const sources = new Map(Object.entries(localDrafts).map(([source, draft]) => [draft.id, source]));
278
+ const directory = join(path, "drafts");
279
+ if (!await exists(directory))
280
+ return [];
281
+ const drafts = [];
282
+ for (const id of await readdir(directory)) {
283
+ const draftDirectory = join(directory, id);
284
+ if (!(await stat(draftDirectory)).isDirectory() || !await exists(join(draftDirectory, "index.html")))
285
+ continue;
286
+ const versions = (await readdir(draftDirectory)).filter((entry) => /^v\d+$/.test(entry)).length;
287
+ drafts.push({ id, versions, source: sources.get(id), url: pageUrls(pages.html_url, id, 1).current });
288
+ }
289
+ return drafts.sort((a, b) => a.id.localeCompare(b.id));
290
+ }
291
+ async function list() {
292
+ const repo = await getRepo();
293
+ await checkTools();
294
+ const drafts = await readPublishedDrafts(repo);
295
+ if (drafts.length === 0)
296
+ return void process.stdout.write(`No drafts published.
297
+ `);
298
+ for (const draft of drafts) {
299
+ process.stdout.write(`${draft.id} ${draft.versions} version${draft.versions === 1 ? "" : "s"} ${draft.source ?? "-"} ${draft.url}
300
+ `);
301
+ }
302
+ }
303
+ async function deleteDrafts() {
304
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
305
+ throw new Error("delete requires an interactive terminal");
306
+ const repo = await getRepo();
307
+ await checkTools();
308
+ const drafts = await readPublishedDrafts(repo);
309
+ if (drafts.length === 0)
310
+ return void process.stdout.write(`No drafts published.
311
+ `);
312
+ drafts.forEach((draft, index) => {
313
+ process.stdout.write(`${index + 1}. ${draft.id} (${draft.versions} version${draft.versions === 1 ? "" : "s"}) ${draft.source ?? ""}
314
+ `);
315
+ });
316
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
317
+ const answer = await prompt.question("Delete which drafts? Enter numbers separated by commas, or press Enter to cancel: ");
318
+ prompt.close();
319
+ const selected = parseSelection(answer, drafts.length).map((choice) => drafts[choice - 1]);
320
+ if (selected.length === 0)
321
+ return void process.stdout.write(`Cancelled.
322
+ `);
323
+ const path = clonePath(repo);
324
+ for (const draft of selected)
325
+ await rm(join(path, "drafts", draft.id), { recursive: true });
326
+ await run("git", ["add", "-A", ...selected.map((draft) => join("drafts", draft.id))], path);
327
+ await run("git", ["commit", "-m", `chore: delete ${selected.map((draft) => draft.id).join(", ")}`], path);
328
+ await run("git", ["push", "origin", "gh-pages"], path);
329
+ const localDrafts = await readJson(draftsPath, {});
330
+ const deleted = new Set(selected.map((draft) => draft.id));
331
+ await writeJson(draftsPath, Object.fromEntries(Object.entries(localDrafts).filter(([, draft]) => !deleted.has(draft.id))));
332
+ process.stdout.write(`Deleted ${selected.length} draft${selected.length === 1 ? "" : "s"}.
333
+ `);
334
+ }
257
335
  async function makeDraftId(path) {
258
336
  let id;
259
337
  do
@@ -356,8 +434,12 @@ async function main(args = process.argv.slice(2)) {
356
434
  `);
357
435
  else if (command.kind === "setup")
358
436
  await setup(command);
359
- else
437
+ else if (command.kind === "publish")
360
438
  await publish(command);
439
+ else if (command.kind === "list")
440
+ await list();
441
+ else
442
+ await deleteDrafts();
361
443
  }
362
444
  var executablePath = process.argv[1] ? await realpath(process.argv[1]).catch(() => resolve(process.argv[1])) : undefined;
363
445
  if (executablePath === fileURLToPath(import.meta.url)) {
@@ -371,5 +453,6 @@ export {
371
453
  main,
372
454
  nextVersion,
373
455
  pageUrls,
374
- parseCommand
456
+ parseCommand,
457
+ parseSelection
375
458
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gh-postplan",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Publish versioned HTML drafts to GitHub Pages",
5
5
  "type": "module",
6
6
  "bin": {
@@ -67,3 +67,12 @@ npx gh-postplan setup OWNER/REPO --create
67
67
  ```
68
68
 
69
69
  All published drafts are public. Configuration and local draft mappings live in `~/.config/gh-postplan`.
70
+
71
+ List or interactively delete published drafts with:
72
+
73
+ ```sh
74
+ npx gh-postplan list
75
+ npx gh-postplan delete
76
+ ```
77
+
78
+ Deleting a draft also deletes all of its permanent versions.