frontpage-host 0.1.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.
Files changed (81) hide show
  1. package/dist/auth/keychain.d.ts +5 -0
  2. package/dist/auth/keychain.js +85 -0
  3. package/dist/bin.d.ts +2 -0
  4. package/dist/bin.js +124 -0
  5. package/dist/commands/analytics.d.ts +1 -0
  6. package/dist/commands/analytics.js +18 -0
  7. package/dist/commands/articles.d.ts +1 -0
  8. package/dist/commands/articles.js +111 -0
  9. package/dist/commands/cat.d.ts +1 -0
  10. package/dist/commands/cat.js +27 -0
  11. package/dist/commands/completion.d.ts +1 -0
  12. package/dist/commands/completion.js +19 -0
  13. package/dist/commands/deploy.d.ts +2 -0
  14. package/dist/commands/deploy.js +32 -0
  15. package/dist/commands/diff.d.ts +1 -0
  16. package/dist/commands/diff.js +93 -0
  17. package/dist/commands/doctor.d.ts +1 -0
  18. package/dist/commands/doctor.js +26 -0
  19. package/dist/commands/edit.d.ts +1 -0
  20. package/dist/commands/edit.js +31 -0
  21. package/dist/commands/extensions.d.ts +1 -0
  22. package/dist/commands/extensions.js +60 -0
  23. package/dist/commands/help.d.ts +1 -0
  24. package/dist/commands/help.js +27 -0
  25. package/dist/commands/history.d.ts +1 -0
  26. package/dist/commands/history.js +49 -0
  27. package/dist/commands/inbox.d.ts +1 -0
  28. package/dist/commands/inbox.js +46 -0
  29. package/dist/commands/login.d.ts +1 -0
  30. package/dist/commands/login.js +38 -0
  31. package/dist/commands/logout.d.ts +1 -0
  32. package/dist/commands/logout.js +7 -0
  33. package/dist/commands/ls.d.ts +1 -0
  34. package/dist/commands/ls.js +19 -0
  35. package/dist/commands/media.d.ts +1 -0
  36. package/dist/commands/media.js +44 -0
  37. package/dist/commands/modules.d.ts +1 -0
  38. package/dist/commands/modules.js +56 -0
  39. package/dist/commands/open.d.ts +1 -0
  40. package/dist/commands/open.js +28 -0
  41. package/dist/commands/pages.d.ts +1 -0
  42. package/dist/commands/pages.js +16 -0
  43. package/dist/commands/prompt.d.ts +1 -0
  44. package/dist/commands/prompt.js +39 -0
  45. package/dist/commands/publish.d.ts +2 -0
  46. package/dist/commands/publish.js +28 -0
  47. package/dist/commands/pull.d.ts +1 -0
  48. package/dist/commands/pull.js +60 -0
  49. package/dist/commands/push.d.ts +1 -0
  50. package/dist/commands/push.js +117 -0
  51. package/dist/commands/revert.d.ts +1 -0
  52. package/dist/commands/revert.js +27 -0
  53. package/dist/commands/runs.d.ts +1 -0
  54. package/dist/commands/runs.js +134 -0
  55. package/dist/commands/search.d.ts +1 -0
  56. package/dist/commands/search.js +30 -0
  57. package/dist/commands/seo.d.ts +1 -0
  58. package/dist/commands/seo.js +57 -0
  59. package/dist/commands/settings.d.ts +1 -0
  60. package/dist/commands/settings.js +65 -0
  61. package/dist/commands/sites.d.ts +1 -0
  62. package/dist/commands/sites.js +20 -0
  63. package/dist/commands/status.d.ts +1 -0
  64. package/dist/commands/status.js +35 -0
  65. package/dist/commands/usage.d.ts +1 -0
  66. package/dist/commands/usage.js +14 -0
  67. package/dist/commands/use.d.ts +1 -0
  68. package/dist/commands/use.js +25 -0
  69. package/dist/commands/whoami.d.ts +1 -0
  70. package/dist/commands/whoami.js +14 -0
  71. package/dist/completions/generate.d.ts +4 -0
  72. package/dist/completions/generate.js +82 -0
  73. package/dist/http/client.d.ts +27 -0
  74. package/dist/http/client.js +163 -0
  75. package/dist/registry.d.ts +14 -0
  76. package/dist/registry.js +116 -0
  77. package/dist/site.d.ts +5 -0
  78. package/dist/site.js +32 -0
  79. package/dist/state/frontpage-state.d.ts +8 -0
  80. package/dist/state/frontpage-state.js +28 -0
  81. package/package.json +40 -0
@@ -0,0 +1,5 @@
1
+ export declare function getApiKey(): Promise<string | null>;
2
+ export declare function setApiKey(secret: string): Promise<void>;
3
+ export declare function clearApiKey(): Promise<void>;
4
+ /** Never print the full key — used by doctor / help hygiene. */
5
+ export declare function maskKey(secret: string): string;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Store / load API keys. Prefers FRONTPAGE_API_KEY, then OS keychain via `security` (macOS)
3
+ * or a restrictive file under ~/.config/frontpage (other platforms).
4
+ */
5
+ import { execFile } from "node:child_process";
6
+ import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { promisify } from "node:util";
10
+ const execFileAsync = promisify(execFile);
11
+ const SERVICE = "frontpage-cli";
12
+ const ACCOUNT = "api-key";
13
+ function filePath() {
14
+ return join(homedir(), ".config", "frontpage", "api-key");
15
+ }
16
+ export async function getApiKey() {
17
+ const fromEnv = process.env.FRONTPAGE_API_KEY?.trim();
18
+ if (fromEnv)
19
+ return fromEnv;
20
+ if (process.platform === "darwin") {
21
+ try {
22
+ const { stdout } = await execFileAsync("security", [
23
+ "find-generic-password",
24
+ "-s",
25
+ SERVICE,
26
+ "-a",
27
+ ACCOUNT,
28
+ "-w",
29
+ ]);
30
+ const key = stdout.trim();
31
+ return key || null;
32
+ }
33
+ catch {
34
+ // fall through to file
35
+ }
36
+ }
37
+ try {
38
+ const raw = (await readFile(filePath(), "utf8")).trim();
39
+ return raw || null;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ export async function setApiKey(secret) {
46
+ const trimmed = secret.trim();
47
+ if (!trimmed)
48
+ throw new Error("API key is empty.");
49
+ if (process.platform === "darwin") {
50
+ try {
51
+ await execFileAsync("security", ["delete-generic-password", "-s", SERVICE, "-a", ACCOUNT]).catch(() => { });
52
+ await execFileAsync("security", [
53
+ "add-generic-password",
54
+ "-s",
55
+ SERVICE,
56
+ "-a",
57
+ ACCOUNT,
58
+ "-w",
59
+ trimmed,
60
+ "-U",
61
+ ]);
62
+ return;
63
+ }
64
+ catch {
65
+ // fall through to file
66
+ }
67
+ }
68
+ const dir = join(homedir(), ".config", "frontpage");
69
+ await mkdir(dir, { recursive: true, mode: 0o700 });
70
+ const path = filePath();
71
+ await writeFile(path, trimmed, { encoding: "utf8", mode: 0o600 });
72
+ await chmod(path, 0o600);
73
+ }
74
+ export async function clearApiKey() {
75
+ if (process.platform === "darwin") {
76
+ await execFileAsync("security", ["delete-generic-password", "-s", SERVICE, "-a", ACCOUNT]).catch(() => { });
77
+ }
78
+ await unlink(filePath()).catch(() => { });
79
+ }
80
+ /** Never print the full key — used by doctor / help hygiene. */
81
+ export function maskKey(secret) {
82
+ if (secret.length <= 12)
83
+ return "****";
84
+ return `${secret.slice(0, 11)}…${secret.slice(-4)}`;
85
+ }
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ import { runHelp } from "./commands/help.js";
3
+ import { runLogin } from "./commands/login.js";
4
+ import { runLogout } from "./commands/logout.js";
5
+ import { runWhoami } from "./commands/whoami.js";
6
+ import { runSites } from "./commands/sites.js";
7
+ import { runUse } from "./commands/use.js";
8
+ import { runDoctor } from "./commands/doctor.js";
9
+ import { runPull } from "./commands/pull.js";
10
+ import { runPush } from "./commands/push.js";
11
+ import { runDiff } from "./commands/diff.js";
12
+ import { runLs } from "./commands/ls.js";
13
+ import { runCat } from "./commands/cat.js";
14
+ import { runSearch } from "./commands/search.js";
15
+ import { runStatus } from "./commands/status.js";
16
+ import { runOpen } from "./commands/open.js";
17
+ import { runPrompt } from "./commands/prompt.js";
18
+ import { runRuns } from "./commands/runs.js";
19
+ import { runPublish, runUnpublish } from "./commands/publish.js";
20
+ import { runDeploy } from "./commands/deploy.js";
21
+ import { runUsage } from "./commands/usage.js";
22
+ import { runAnalytics } from "./commands/analytics.js";
23
+ import { runPages } from "./commands/pages.js";
24
+ import { runModules } from "./commands/modules.js";
25
+ import { runExtensions } from "./commands/extensions.js";
26
+ import { runMedia } from "./commands/media.js";
27
+ import { runInbox } from "./commands/inbox.js";
28
+ import { runSeo } from "./commands/seo.js";
29
+ import { runArticles } from "./commands/articles.js";
30
+ import { runSettings } from "./commands/settings.js";
31
+ import { runEdit } from "./commands/edit.js";
32
+ import { runHistory } from "./commands/history.js";
33
+ import { runRevert } from "./commands/revert.js";
34
+ import { runCompletion } from "./commands/completion.js";
35
+ import { EXIT_USAGE, EXIT_UNEXPECTED } from "./http/client.js";
36
+ async function main() {
37
+ const argv = process.argv.slice(2);
38
+ const cmd = argv[0];
39
+ const args = argv.slice(1);
40
+ if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
41
+ return runHelp(cmd === "help" ? args : []);
42
+ }
43
+ switch (cmd) {
44
+ case "login":
45
+ return runLogin(args);
46
+ case "logout":
47
+ return runLogout();
48
+ case "whoami":
49
+ return runWhoami();
50
+ case "sites":
51
+ return runSites();
52
+ case "use":
53
+ case "link":
54
+ return runUse(args);
55
+ case "doctor":
56
+ return runDoctor();
57
+ case "pull":
58
+ return runPull(args);
59
+ case "push":
60
+ return runPush(args);
61
+ case "diff":
62
+ return runDiff(args);
63
+ case "ls":
64
+ return runLs(args);
65
+ case "cat":
66
+ return runCat(args);
67
+ case "search":
68
+ return runSearch(args);
69
+ case "status":
70
+ return runStatus(args);
71
+ case "open":
72
+ return runOpen(args);
73
+ case "prompt":
74
+ return runPrompt(args);
75
+ case "runs":
76
+ return runRuns(args);
77
+ case "publish":
78
+ return runPublish(args);
79
+ case "unpublish":
80
+ return runUnpublish(args);
81
+ case "deploy":
82
+ return runDeploy(args);
83
+ case "usage":
84
+ return runUsage(args);
85
+ case "analytics":
86
+ return runAnalytics(args);
87
+ case "pages":
88
+ return runPages(args);
89
+ case "modules":
90
+ return runModules(args);
91
+ case "extensions":
92
+ return runExtensions(args);
93
+ case "media":
94
+ return runMedia(args);
95
+ case "inbox":
96
+ return runInbox(args);
97
+ case "seo":
98
+ return runSeo(args);
99
+ case "articles":
100
+ return runArticles(args);
101
+ case "settings":
102
+ return runSettings(args);
103
+ case "edit":
104
+ return runEdit(args);
105
+ case "history":
106
+ return runHistory(args);
107
+ case "revert":
108
+ return runRevert(args);
109
+ case "completion":
110
+ return runCompletion(args);
111
+ default:
112
+ console.error(`Unknown command: ${cmd}`);
113
+ console.error("Run: frontpage help");
114
+ return EXIT_USAGE;
115
+ }
116
+ }
117
+ main()
118
+ .then((code) => {
119
+ process.exitCode = code;
120
+ })
121
+ .catch((err) => {
122
+ console.error(err instanceof Error ? err.message : String(err));
123
+ process.exitCode = EXIT_UNEXPECTED;
124
+ });
@@ -0,0 +1 @@
1
+ export declare function runAnalytics(args: string[]): Promise<number>;
@@ -0,0 +1,18 @@
1
+ import { resolveSlug } from "../site.js";
2
+ import { EXIT_OK, exitCodeForError, v1Get } from "../http/client.js";
3
+ export async function runAnalytics(args) {
4
+ try {
5
+ const { slug, rest } = await resolveSlug(args);
6
+ let days = "7";
7
+ const daysIdx = rest.indexOf("--days");
8
+ if (daysIdx >= 0 && rest[daysIdx + 1])
9
+ days = rest[daysIdx + 1];
10
+ const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/analytics?days=${encodeURIComponent(days)}`);
11
+ console.log(JSON.stringify(data.dashboard, null, 2));
12
+ return EXIT_OK;
13
+ }
14
+ catch (err) {
15
+ console.error(err instanceof Error ? err.message : String(err));
16
+ return exitCodeForError(err);
17
+ }
18
+ }
@@ -0,0 +1 @@
1
+ export declare function runArticles(args: string[]): Promise<number>;
@@ -0,0 +1,111 @@
1
+ import { resolveSlug } from "../site.js";
2
+ import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get, v1Post } from "../http/client.js";
3
+ function takeFlag(rest, name) {
4
+ const i = rest.indexOf(name);
5
+ if (i === -1)
6
+ return undefined;
7
+ const v = rest[i + 1];
8
+ rest.splice(i, 2);
9
+ return v;
10
+ }
11
+ async function postOp(slug, op, extra) {
12
+ return v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/articles`, {
13
+ op,
14
+ ...extra,
15
+ });
16
+ }
17
+ export async function runArticles(args) {
18
+ try {
19
+ const { slug, rest } = await resolveSlug(args);
20
+ const sub = rest[0] ?? "list";
21
+ if (sub === "list" || sub === "ls") {
22
+ const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/articles`);
23
+ console.log(JSON.stringify(data, null, 2));
24
+ return EXIT_OK;
25
+ }
26
+ if (sub === "content") {
27
+ const action = rest[1];
28
+ const work = rest.slice(2);
29
+ const section = takeFlag(work, "--section") ?? work[0];
30
+ const articleSlug = takeFlag(work, "--article") ?? takeFlag(work, "--slug") ?? work[1];
31
+ if (!section || !articleSlug) {
32
+ console.error("Usage: frontpage articles content get|set --section <sec> --article <slug> …");
33
+ return EXIT_USAGE;
34
+ }
35
+ if (action === "get") {
36
+ const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/articles/content?section=${encodeURIComponent(section)}&articleSlug=${encodeURIComponent(articleSlug)}`);
37
+ console.log(JSON.stringify(data, null, 2));
38
+ return EXIT_OK;
39
+ }
40
+ if (action === "set") {
41
+ const body = takeFlag(work, "--body");
42
+ const baseCommitSha = takeFlag(work, "--base");
43
+ if (body === undefined || !baseCommitSha) {
44
+ console.error("Usage: frontpage articles content set --section … --article … --body … --base <sha>");
45
+ return EXIT_USAGE;
46
+ }
47
+ const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/articles/content`, { section, articleSlug, body, baseCommitSha });
48
+ console.log(JSON.stringify(data, null, 2));
49
+ return EXIT_OK;
50
+ }
51
+ console.error("Usage: frontpage articles content get|set …");
52
+ return EXIT_USAGE;
53
+ }
54
+ const work = rest.slice(1);
55
+ const opMap = {
56
+ "create-section": "create-section",
57
+ "config-section": "config-section",
58
+ "archive-section": "archive-section",
59
+ "restore-section": "restore-section",
60
+ "create-article": "create-article",
61
+ "delete-article": "delete-article",
62
+ "set-draft": "set-article-draft",
63
+ "set-image": "set-article-image",
64
+ "set-tags": "set-article-tags",
65
+ "set-seo": "set-article-seo",
66
+ "set-meta": "set-article-meta",
67
+ };
68
+ const op = opMap[sub];
69
+ if (!op) {
70
+ console.error("Usage: frontpage articles list|content|create-section|create-article|set-draft|… [--site <slug>]");
71
+ return EXIT_USAGE;
72
+ }
73
+ const body = {};
74
+ const section = takeFlag(work, "--section") ?? takeFlag(work, "--instance");
75
+ if (section) {
76
+ body.section = section;
77
+ body.instanceId = section;
78
+ }
79
+ for (const [flag, key] of [
80
+ ["--label", "label"],
81
+ ["--route", "routePrefix"],
82
+ ["--title", "title"],
83
+ ["--article", "articleSlug"],
84
+ ["--description", "description"],
85
+ ["--featured-image", "featuredImage"],
86
+ ["--image-alt", "imageAlt"],
87
+ ["--seo-title", "seoTitle"],
88
+ ["--seo-description", "seoDescription"],
89
+ ["--author", "author"],
90
+ ]) {
91
+ const v = takeFlag(work, flag);
92
+ if (v !== undefined)
93
+ body[key] = v;
94
+ }
95
+ const draft = takeFlag(work, "--draft");
96
+ if (draft !== undefined)
97
+ body.draft = draft === "true" || draft === "1";
98
+ const tags = takeFlag(work, "--tags");
99
+ if (tags !== undefined)
100
+ body.tags = tags.split(",").map((t) => t.trim()).filter(Boolean);
101
+ if (work.includes("--clear-image"))
102
+ body.clearImage = true;
103
+ const data = await postOp(slug, op, body);
104
+ console.log(JSON.stringify(data, null, 2));
105
+ return EXIT_OK;
106
+ }
107
+ catch (err) {
108
+ console.error(err instanceof Error ? err.message : String(err));
109
+ return exitCodeForError(err);
110
+ }
111
+ }
@@ -0,0 +1 @@
1
+ export declare function runCat(args: string[]): Promise<number>;
@@ -0,0 +1,27 @@
1
+ import { resolveSlug } from "../site.js";
2
+ import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get } from "../http/client.js";
3
+ export async function runCat(args) {
4
+ try {
5
+ const { slug, rest } = await resolveSlug(args);
6
+ const path = rest[0];
7
+ if (!path) {
8
+ console.error("Usage: frontpage cat <path> [--site <slug>]");
9
+ return EXIT_USAGE;
10
+ }
11
+ const encPath = path.split("/").map(encodeURIComponent).join("/");
12
+ const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/files/${encPath}`);
13
+ if (data.file.encoding === "base64") {
14
+ process.stdout.write(Buffer.from(data.file.content, "base64"));
15
+ }
16
+ else {
17
+ process.stdout.write(data.file.content);
18
+ if (!data.file.content.endsWith("\n"))
19
+ process.stdout.write("\n");
20
+ }
21
+ return EXIT_OK;
22
+ }
23
+ catch (err) {
24
+ console.error(err instanceof Error ? err.message : String(err));
25
+ return exitCodeForError(err);
26
+ }
27
+ }
@@ -0,0 +1 @@
1
+ export declare function runCompletion(args: string[]): number;
@@ -0,0 +1,19 @@
1
+ import { EXIT_OK, EXIT_USAGE } from "../http/client.js";
2
+ import { generateCompletion } from "../completions/generate.js";
3
+ export function runCompletion(args) {
4
+ const shell = (args[0] ?? "").trim().toLowerCase();
5
+ if (!shell || shell === "--help" || shell === "-h") {
6
+ console.error("Usage: frontpage completion bash|zsh|fish");
7
+ console.error(' eval "$(frontpage completion bash)"');
8
+ console.error(' eval "$(frontpage completion zsh)"');
9
+ console.error(" frontpage completion fish | source");
10
+ return EXIT_USAGE;
11
+ }
12
+ const script = generateCompletion(shell);
13
+ if (!script) {
14
+ console.error(`Unknown shell: ${shell}. Use bash, zsh, or fish.`);
15
+ return EXIT_USAGE;
16
+ }
17
+ process.stdout.write(script);
18
+ return EXIT_OK;
19
+ }
@@ -0,0 +1,2 @@
1
+ /** `frontpage deploy wait` — wait until no run is in deploying/queued/running. */
2
+ export declare function runDeploy(args: string[]): Promise<number>;
@@ -0,0 +1,32 @@
1
+ import { resolveSlug } from "../site.js";
2
+ import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get } from "../http/client.js";
3
+ /** `frontpage deploy wait` — wait until no run is in deploying/queued/running. */
4
+ export async function runDeploy(args) {
5
+ try {
6
+ const sub = args[0];
7
+ if (sub !== "wait") {
8
+ console.error("Usage: frontpage deploy wait [--site <slug>]");
9
+ return EXIT_USAGE;
10
+ }
11
+ const { slug } = await resolveSlug(args.slice(1));
12
+ const started = Date.now();
13
+ const maxMs = 30 * 60 * 1000;
14
+ while (Date.now() - started < maxMs) {
15
+ const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs`);
16
+ const busy = (data.active ?? []).filter((r) => ["queued", "running", "deploying"].includes(r.status));
17
+ if (busy.length === 0) {
18
+ console.log(data.run ? `${data.run.id} ${data.run.status}` : "No active deploy.");
19
+ return EXIT_OK;
20
+ }
21
+ const head = busy.find((r) => r.status === "deploying") ?? busy[0];
22
+ process.stderr.write(`\r${head.status} ${head.statusMessage ?? ""} `);
23
+ await new Promise((r) => setTimeout(r, 2000));
24
+ }
25
+ console.error("Timed out waiting for deploy.");
26
+ return EXIT_USAGE;
27
+ }
28
+ catch (err) {
29
+ console.error(err instanceof Error ? err.message : String(err));
30
+ return exitCodeForError(err);
31
+ }
32
+ }
@@ -0,0 +1 @@
1
+ export declare function runDiff(args: string[]): Promise<number>;
@@ -0,0 +1,93 @@
1
+ import { readFile, readdir, stat } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ import { join, relative, sep } from "node:path";
4
+ import { resolveSlug } from "../site.js";
5
+ import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get } from "../http/client.js";
6
+ const ROOTS = ["src", "public", "content"];
7
+ async function walkLocal(cwd) {
8
+ const map = new Map();
9
+ async function walk(abs) {
10
+ let entries;
11
+ try {
12
+ entries = await readdir(abs, { withFileTypes: true });
13
+ }
14
+ catch {
15
+ return;
16
+ }
17
+ for (const ent of entries) {
18
+ const child = join(abs, ent.name);
19
+ if (ent.isDirectory()) {
20
+ if (ent.name === "node_modules" || ent.name === ".git" || ent.name === ".frontpage")
21
+ continue;
22
+ await walk(child);
23
+ }
24
+ else if (ent.isFile()) {
25
+ const rel = relative(cwd, child).split(sep).join("/");
26
+ if (!ROOTS.some((r) => rel === r || rel.startsWith(`${r}/`)))
27
+ continue;
28
+ const buf = await readFile(child);
29
+ map.set(rel, createHash("sha256").update(buf).digest("hex"));
30
+ }
31
+ }
32
+ }
33
+ for (const root of ROOTS)
34
+ await walk(join(cwd, root));
35
+ return map;
36
+ }
37
+ function remoteHash(file) {
38
+ const buf = file.encoding === "base64"
39
+ ? Buffer.from(file.content, "base64")
40
+ : Buffer.from(file.content, "utf8");
41
+ return createHash("sha256").update(buf).digest("hex");
42
+ }
43
+ export async function runDiff(args) {
44
+ try {
45
+ const { slug } = await resolveSlug(args);
46
+ const cwd = process.cwd();
47
+ const local = await walkLocal(cwd);
48
+ const listing = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/files`);
49
+ const remotePaths = new Set(listing.files.map((f) => f.path));
50
+ let changed = 0;
51
+ let onlyLocal = 0;
52
+ let onlyRemote = 0;
53
+ for (const path of [...local.keys()].sort()) {
54
+ if (!remotePaths.has(path)) {
55
+ console.log(`A ${path}`);
56
+ onlyLocal++;
57
+ continue;
58
+ }
59
+ const encPath = path.split("/").map(encodeURIComponent).join("/");
60
+ const remote = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/files/${encPath}`);
61
+ const rh = remoteHash(remote.file);
62
+ const lh = local.get(path);
63
+ if (rh !== lh) {
64
+ console.log(`M ${path}`);
65
+ changed++;
66
+ }
67
+ }
68
+ for (const f of listing.files) {
69
+ if (!local.has(f.path)) {
70
+ console.log(`D ${f.path}`);
71
+ onlyRemote++;
72
+ }
73
+ }
74
+ const total = changed + onlyLocal + onlyRemote;
75
+ if (total === 0) {
76
+ console.log("No differences.");
77
+ }
78
+ else {
79
+ console.error(`# ${total} difference(s) · tip ${(listing.baseCommitSha ?? "").slice(0, 7) || "?"} · A=local-only M=modified D=remote-only`);
80
+ }
81
+ // Ensure cwd has the tree (optional UX check)
82
+ void (await stat(cwd).catch(() => null));
83
+ return EXIT_OK;
84
+ }
85
+ catch (err) {
86
+ if (err instanceof Error && /No site selected/.test(err.message)) {
87
+ console.error(err.message);
88
+ return EXIT_USAGE;
89
+ }
90
+ console.error(err instanceof Error ? err.message : String(err));
91
+ return exitCodeForError(err);
92
+ }
93
+ }
@@ -0,0 +1 @@
1
+ export declare function runDoctor(): Promise<number>;
@@ -0,0 +1,26 @@
1
+ import { getApiKey, maskKey } from "../auth/keychain.js";
2
+ import { apiBase, EXIT_OK, EXIT_AUTH, EXIT_UNEXPECTED, v1Get } from "../http/client.js";
3
+ import { readState } from "../state/frontpage-state.js";
4
+ export async function runDoctor() {
5
+ console.log(`API base: ${apiBase()}`);
6
+ const key = await getApiKey();
7
+ if (!key) {
8
+ console.error("No API key. Run: frontpage login");
9
+ return EXIT_AUTH;
10
+ }
11
+ console.log(`API key: ${maskKey(key)} (never prints the full secret)`);
12
+ const state = await readState();
13
+ console.log(`Folder site: ${state.slug ?? "(not set — run frontpage use <subdomain>)"}`);
14
+ try {
15
+ const me = await v1Get("/api/v1/me");
16
+ console.log(`whoami: ok (${me.email ?? me.userId})`);
17
+ const sites = await v1Get("/api/v1/sites");
18
+ console.log(`sites: ok (${sites.sites.length} site(s))`);
19
+ console.log("doctor: all checks passed");
20
+ return EXIT_OK;
21
+ }
22
+ catch (err) {
23
+ console.error(err instanceof Error ? err.message : String(err));
24
+ return EXIT_UNEXPECTED;
25
+ }
26
+ }
@@ -0,0 +1 @@
1
+ export declare function runEdit(args: string[]): Promise<number>;
@@ -0,0 +1,31 @@
1
+ import { resolveSlug } from "../site.js";
2
+ import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Post } from "../http/client.js";
3
+ function takeFlag(rest, name) {
4
+ const i = rest.indexOf(name);
5
+ if (i === -1)
6
+ return undefined;
7
+ const v = rest[i + 1];
8
+ rest.splice(i, 2);
9
+ return v;
10
+ }
11
+ export async function runEdit(args) {
12
+ try {
13
+ const { slug, rest } = await resolveSlug(args);
14
+ const work = [...rest];
15
+ const path = takeFlag(work, "--path") ?? "/";
16
+ const oldText = takeFlag(work, "--old") ?? takeFlag(work, "--old-text");
17
+ const newText = takeFlag(work, "--new") ?? takeFlag(work, "--new-text");
18
+ if (!oldText || !newText) {
19
+ console.error('Usage: frontpage edit --path / --old "exact text" --new "replacement" [--site <slug>]');
20
+ return EXIT_USAGE;
21
+ }
22
+ const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/edit`, { path, oldText, newText });
23
+ console.log(`${data.commitHash.slice(0, 7)} ${data.sourcePath}`);
24
+ console.log(data.message);
25
+ return EXIT_OK;
26
+ }
27
+ catch (err) {
28
+ console.error(err instanceof Error ? err.message : String(err));
29
+ return exitCodeForError(err);
30
+ }
31
+ }
@@ -0,0 +1 @@
1
+ export declare function runExtensions(args: string[]): Promise<number>;