gh-postplan 0.1.0 → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cyrus Venn Casada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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.0",
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.