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 +9 -0
- package/dist/cli.js +87 -4
- package/package.json +1 -1
- package/skills/gh-postplan/SKILL.md +9 -0
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.
|
|
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
|
@@ -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.
|