frontpage-host 0.1.2 → 0.1.3

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/AGENTS.md CHANGED
@@ -66,12 +66,15 @@ frontpage use <subdomain>
66
66
 
67
67
  ## File sync recipe
68
68
 
69
- Allowlisted roots only: `src/`, `public/`, `content/`. Other paths never sync (including some platform-owned scaffold files).
69
+ Allowlisted roots only: `src/`, `public/`, `content/`. Other paths never sync (including platform-owned scaffold/overlay files such as `BaseLayout.astro`, `Modules.astro`, and `src/components/extensions/**`). Do not store secrets in the site tree (`.env`, `*.pem`, `secrets/`, `credentials.json`) — the API rejects those paths.
70
70
 
71
71
  ```bash
72
+ # Session start / before editing: refresh if tip may have moved
73
+ frontpage status --json # data.tipDrift === true → must pull
74
+ # or just:
72
75
  frontpage pull
73
76
  # edit local allowlisted files
74
- frontpage diff --json # data.dirty === true when local ≠ remote
77
+ frontpage diff --json # data.dirty / data.tipDrift; compares git blob SHAs (one list GET)
75
78
  frontpage push --message "…"
76
79
  # on exit 3 (CAS / active run): frontpage status --json → pull or wait → retry push
77
80
  # Review at https://<subdomain>.frontpage.host
@@ -80,12 +83,14 @@ frontpage push --message "…"
80
83
 
81
84
  Rules:
82
85
 
83
- 1. **Pull before push** so `baseCommitSha` is set (CAS).
84
- 2. **Do not push** while an agent run is active (`status` / `runs`).
85
- 3. Push updates **staging** (subdomain). Production is unchanged until someone runs `publish`.
86
- 4. Pushing module markup may auto-activate modules (same as the editor agent).
87
- 5. Binaries use base64; do not UTF-8 round-trip them.
88
- 6. Prefer reviewing the subdomain after push; only use `deploy wait` if you need to block until a build finishes.
86
+ 1. **Pull before edit** when the remote tip may have moved (UI agent, another machine, or you pushed elsewhere). Check `status --json` / `diff --json` → `tipDrift`, or pull at session start. Stale local trees are not auto-healed; CAS only blocks a bad **push**.
87
+ 2. **Pull before push** so `baseCommitSha` is set (CAS). Still required even if you already pulled to edit.
88
+ 3. **Do not push** while an agent run is active (`status` / `runs`).
89
+ 4. Push updates **staging** (subdomain). Production is unchanged until someone runs `publish`.
90
+ 5. Pushing module markup may auto-activate modules (same as the editor agent).
91
+ 6. Binaries use base64; do not UTF-8 round-trip them.
92
+ 7. Prefer reviewing the subdomain after push; only use `deploy wait` if you need to block until a build finishes.
93
+ 8. The **UI / canvas agent does not use this CLI** — it edits the live tip in-process. Only CLI checkouts need `pull`.
89
94
 
90
95
  ## Agent prompt loop
91
96
 
@@ -1,9 +1,10 @@
1
- import { readFile, readdir, stat } from "node:fs/promises";
2
- import { createHash } from "node:crypto";
1
+ import { readFile, readdir } from "node:fs/promises";
3
2
  import { join, relative, sep } from "node:path";
4
3
  import { resolveSlug } from "../site.js";
5
4
  import { EXIT_OK, EXIT_USAGE, v1Get } from "../http/client.js";
6
5
  import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
6
+ import { readState } from "../state/frontpage-state.js";
7
+ import { gitBlobSha, isGitBlobSha } from "../git-blob-sha.js";
7
8
  const ROOTS = ["src", "public", "content"];
8
9
  async function walkLocal(cwd) {
9
10
  const map = new Map();
@@ -27,7 +28,7 @@ async function walkLocal(cwd) {
27
28
  if (!ROOTS.some((r) => rel === r || rel.startsWith(`${r}/`)))
28
29
  continue;
29
30
  const buf = await readFile(child);
30
- map.set(rel, createHash("sha256").update(buf).digest("hex"));
31
+ map.set(rel, gitBlobSha(buf));
31
32
  }
32
33
  }
33
34
  }
@@ -35,32 +36,32 @@ async function walkLocal(cwd) {
35
36
  await walk(join(cwd, root));
36
37
  return map;
37
38
  }
38
- function remoteHash(file) {
39
- const buf = file.encoding === "base64"
40
- ? Buffer.from(file.content, "base64")
41
- : Buffer.from(file.content, "utf8");
42
- return createHash("sha256").update(buf).digest("hex");
39
+ function assertRemoteBlobShas(files) {
40
+ for (const f of files) {
41
+ if (!isGitBlobSha(f.blobSha)) {
42
+ throw new Error("Remote file list is missing blobSha. Upgrade Frontpage (API) — this CLI requires git blob SHAs on GET /files and will not fall back to per-file downloads.");
43
+ }
44
+ }
43
45
  }
44
46
  export async function runDiff(args) {
45
47
  try {
46
48
  const { slug } = await resolveSlug(args);
47
49
  const cwd = process.cwd();
50
+ const state = await readState(cwd);
48
51
  const local = await walkLocal(cwd);
49
52
  const listing = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/files`);
50
- const remotePaths = new Set(listing.files.map((f) => f.path));
53
+ assertRemoteBlobShas(listing.files);
54
+ const remoteByPath = new Map(listing.files.map((f) => [f.path, f.blobSha]));
51
55
  const added = [];
52
56
  const modified = [];
53
57
  const deleted = [];
54
58
  for (const path of [...local.keys()].sort()) {
55
- if (!remotePaths.has(path)) {
59
+ const remoteSha = remoteByPath.get(path);
60
+ if (!remoteSha) {
56
61
  added.push(path);
57
62
  continue;
58
63
  }
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
+ if (remoteSha.toLowerCase() !== local.get(path).toLowerCase())
64
65
  modified.push(path);
65
66
  }
66
67
  for (const f of listing.files) {
@@ -68,12 +69,16 @@ export async function runDiff(args) {
68
69
  deleted.push(f.path);
69
70
  }
70
71
  const dirty = added.length + modified.length + deleted.length > 0;
71
- void (await stat(cwd).catch(() => null));
72
+ const tipDrift = Boolean(state.baseCommitSha &&
73
+ listing.baseCommitSha &&
74
+ state.baseCommitSha !== listing.baseCommitSha);
72
75
  if (isJson()) {
73
76
  emitOk("diff", {
74
77
  slug,
75
78
  baseCommitSha: listing.baseCommitSha,
76
79
  dirty,
80
+ tipDrift,
81
+ comparedWith: "blobSha",
77
82
  added,
78
83
  modified,
79
84
  deleted,
@@ -92,6 +97,9 @@ export async function runDiff(args) {
92
97
  else {
93
98
  console.error(`# ${added.length + modified.length + deleted.length} difference(s) · tip ${(listing.baseCommitSha ?? "").slice(0, 7) || "?"} · A=local-only M=modified D=remote-only`);
94
99
  }
100
+ if (tipDrift) {
101
+ console.error("# tipDrift: remote tip moved since last pull — run frontpage pull before editing further");
102
+ }
95
103
  return EXIT_OK;
96
104
  }
97
105
  catch (err) {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Git blob object SHA-1 — same digest as `git hash-object` / GitHub tree `sha`.
3
+ * Header is `blob <byteLength>\0` followed by raw file bytes (not UTF-8 reinterpreted).
4
+ */
5
+ export declare function gitBlobSha(content: Buffer): string;
6
+ /** True when a remote list entry carries a usable git blob SHA. */
7
+ export declare function isGitBlobSha(value: unknown): value is string;
@@ -0,0 +1,14 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * Git blob object SHA-1 — same digest as `git hash-object` / GitHub tree `sha`.
4
+ * Header is `blob <byteLength>\0` followed by raw file bytes (not UTF-8 reinterpreted).
5
+ */
6
+ export function gitBlobSha(content) {
7
+ const header = Buffer.from(`blob ${content.length}\0`, "utf8");
8
+ return createHash("sha1").update(header).update(content).digest("hex");
9
+ }
10
+ const BLOB_SHA_RE = /^[0-9a-f]{40}$/i;
11
+ /** True when a remote list entry carries a usable git blob SHA. */
12
+ export function isGitBlobSha(value) {
13
+ return typeof value === "string" && BLOB_SHA_RE.test(value);
14
+ }
package/dist/registry.js CHANGED
@@ -86,11 +86,14 @@ export const COMMANDS = [
86
86
  },
87
87
  {
88
88
  name: "diff",
89
- summary: "Compare local allowlisted files to the remote tip",
89
+ summary: "Compare local allowlisted files to the remote tip (git blob SHA)",
90
90
  usage: "frontpage diff [--site <slug>] [--json]",
91
91
  topics: ["sync"],
92
92
  examples: ["frontpage diff --json"],
93
- flags: ["--site <slug>", "JSON data.dirty is true when differences exist"],
93
+ flags: [
94
+ "--site <slug>",
95
+ "JSON data.dirty when local ≠ remote; data.tipDrift when last pull tip ≠ remote tip",
96
+ ],
94
97
  },
95
98
  {
96
99
  name: "ls",
@@ -326,21 +329,24 @@ export const TOPICS = {
326
329
  PUBLIC_CLI_ENABLED is server-side — the CLI cannot enable it.`,
327
330
  sync: `File sync
328
331
  frontpage use <subdomain>
332
+ frontpage status # tipDrift when remote moved since last pull
329
333
  frontpage pull # downloads src/, public/, content/ into cwd
330
- frontpage diff # local vs remote tip (--json → data.dirty)
334
+ frontpage diff # local vs remote via git blobSha (--json → dirty + tipDrift)
331
335
  frontpage push [--message …] # uploads allowlisted files; CAS + active-run guard
332
336
  frontpage ls [prefix]
333
337
  frontpage cat <path>
334
338
  frontpage search <pattern>
335
- frontpage status
336
339
  frontpage open # opens https://<slug>.frontpage.host/ (--prod is a no-op stub)
337
340
 
338
341
  Local state: .frontpage/state.json { slug, baseCommitSha }
339
- Pull first so push can compare-and-swap on baseCommitSha.
340
- Do not push while the editor agent is building (status / runs show active work).
342
+ Pull BEFORE EDITING when tip may have moved (UI agent / another checkout) — check tipDrift.
343
+ Pull before push so CAS has baseCommitSha. Do not push while the editor agent is building.
344
+ The UI/canvas agent does not use this CLI; only CLI checkouts need pull.
341
345
  Pushing module markup may auto-activate modules (same as the agent).
342
346
  tokens.ts may be auto-repaired at commit if it would break the build.
343
347
  Binaries use base64 (not utf8-corrupted).
348
+ Platform overlay files (BaseLayout, Modules, Analytics, extensions, …) are not syncable.
349
+ Do not store secrets in the site tree (.env, *.pem, secrets/, credentials.json) — rejected by the API.
344
350
 
345
351
  Staging vs production:
346
352
  - Push/prompt/edit update STAGING: https://<subdomain>.frontpage.host
package/llms.txt CHANGED
@@ -8,7 +8,7 @@
8
8
  - Site id: subdomain only (frontpage use <subdomain>). Not custom domains.
9
9
  - Staging: https://<subdomain>.frontpage.host — push/prompt/edit update staging; no separate staging deploy step for agents
10
10
  - Production: custom domain after frontpage publish — NEVER publish/unpublish unless the user explicitly asks
11
- - Sync: allowlist src/ public/ content/; pull → edit → push (CAS); no push during active agent run; review on subdomain
11
+ - Sync: allowlist src/ public/ content/; platform overlay + secret-like paths rejected; pull before edit if tip may have moved (status/diff tipDrift); pull → edit → diff (blobSha) → push (CAS); no push during active agent run; UI agent does not use CLI pull; review on subdomain
12
12
  - Machine: global --json / --quiet; exit 0/2/3/4/5 (see help exit-codes)
13
13
  - deploy wait = optional build wait only; it is NOT go-live / publish
14
14
  - No runs continue, domains, billing, members, MCP, or git remotes in v1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontpage-host",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Frontpage CLI — manage your site from the terminal",
5
5
  "type": "module",
6
6
  "bin": {