frontpage-host 0.1.0 → 0.1.2
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 +194 -0
- package/README.md +57 -0
- package/dist/bin.js +32 -5
- package/dist/commands/deploy.js +18 -9
- package/dist/commands/diff.js +33 -23
- package/dist/commands/doctor.js +34 -9
- package/dist/commands/help.js +70 -9
- package/dist/commands/history.js +10 -7
- package/dist/commands/ls.js +7 -3
- package/dist/commands/open.js +16 -12
- package/dist/commands/pages.js +10 -5
- package/dist/commands/prompt.js +13 -10
- package/dist/commands/publish.js +21 -9
- package/dist/commands/pull.js +21 -11
- package/dist/commands/push.js +15 -7
- package/dist/commands/revert.js +15 -10
- package/dist/commands/runs.js +52 -24
- package/dist/commands/sites.js +8 -3
- package/dist/commands/status.js +20 -5
- package/dist/commands/whoami.js +12 -6
- package/dist/completions/generate.js +5 -4
- package/dist/flags.d.ts +13 -0
- package/dist/flags.js +31 -0
- package/dist/registry.d.ts +5 -0
- package/dist/registry.js +347 -49
- package/dist/runtime.d.ts +22 -0
- package/dist/runtime.js +73 -0
- package/llms.txt +14 -0
- package/package.json +5 -2
package/AGENTS.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Frontpage CLI — agent operating manual
|
|
2
|
+
|
|
3
|
+
This file ships inside the `frontpage-host` npm package. Prefer it over scraping the product website.
|
|
4
|
+
|
|
5
|
+
## Install and invoke
|
|
6
|
+
|
|
7
|
+
| Item | Value |
|
|
8
|
+
|------|--------|
|
|
9
|
+
| npm package | `frontpage-host` |
|
|
10
|
+
| Binary | `frontpage` |
|
|
11
|
+
| Wrong package | Do **not** install npm `frontpage` (unrelated) |
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i -g frontpage-host
|
|
15
|
+
frontpage help --json # introspect commands + topics
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Node.js ≥ 20.
|
|
19
|
+
|
|
20
|
+
## Staging vs production (read this first)
|
|
21
|
+
|
|
22
|
+
Frontpage has two surfaces:
|
|
23
|
+
|
|
24
|
+
| Surface | URL | What it is |
|
|
25
|
+
|---------|-----|------------|
|
|
26
|
+
| **Staging** | `https://<subdomain>.frontpage.host` | Working copy for edits, previews, and review |
|
|
27
|
+
| **Production** | Custom domain (when connected), after **publish** | What end customers see on the live domain |
|
|
28
|
+
|
|
29
|
+
**How changes become visible on staging**
|
|
30
|
+
|
|
31
|
+
- `frontpage push`, `frontpage prompt`, and `frontpage edit` update the site branch and the **staging** surface.
|
|
32
|
+
- After a push or agent run finishes building, changes are visible at `https://<subdomain>.frontpage.host`.
|
|
33
|
+
- There is **no separate “deploy to staging” step** agents must run. Do not treat `frontpage deploy wait` as “going live.” At most it waits for an in-flight build to finish before you open the subdomain to review.
|
|
34
|
+
- `frontpage open` opens the subdomain staging URL.
|
|
35
|
+
|
|
36
|
+
**Production publish is a deliberate promote**
|
|
37
|
+
|
|
38
|
+
- `frontpage publish` copies the current **staging tip** to **production** (custom domain / published site).
|
|
39
|
+
- **Never run `publish` or `unpublish` unless the user explicitly asked**, or the user’s own automation/workflow clearly requires autonomous publishing.
|
|
40
|
+
- Default: finish work on staging, tell the user to review `https://<subdomain>.frontpage.host`, and let **them** publish (or ask you to publish after they approve).
|
|
41
|
+
|
|
42
|
+
## Non-interactive auth
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
frontpage login "$FRONTPAGE_API_KEY"
|
|
46
|
+
# or export FRONTPAGE_API_KEY=fp_live_… (wins over keychain)
|
|
47
|
+
frontpage whoami --json
|
|
48
|
+
frontpage doctor --json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- Keys are minted only in the Frontpage UI (profile → Developers). Prefix `fp_live_`.
|
|
52
|
+
- Keys act as the minting user on **every** site they can access. Treat like passwords.
|
|
53
|
+
- `PUBLIC_CLI_ENABLED` is server-side; the CLI cannot turn it on. Bare “not found” on `/api/v1/me` usually means the flag is off (exit 5).
|
|
54
|
+
|
|
55
|
+
## Site IDs
|
|
56
|
+
|
|
57
|
+
- Always use the **subdomain** (`arbiter`, `asimov-press`) with `use` / `--site`.
|
|
58
|
+
- Never pass custom domains, FQDNs, or URLs as the site id.
|
|
59
|
+
- Folder state: `.frontpage/state.json` → `{ slug, baseCommitSha }`.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
frontpage sites --json
|
|
63
|
+
frontpage use <subdomain>
|
|
64
|
+
# or: frontpage <cmd> --site <subdomain>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## File sync recipe
|
|
68
|
+
|
|
69
|
+
Allowlisted roots only: `src/`, `public/`, `content/`. Other paths never sync (including some platform-owned scaffold files).
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
frontpage pull
|
|
73
|
+
# edit local allowlisted files
|
|
74
|
+
frontpage diff --json # data.dirty === true when local ≠ remote
|
|
75
|
+
frontpage push --message "…"
|
|
76
|
+
# on exit 3 (CAS / active run): frontpage status --json → pull or wait → retry push
|
|
77
|
+
# Review at https://<subdomain>.frontpage.host
|
|
78
|
+
# Do NOT publish unless the user asked.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Rules:
|
|
82
|
+
|
|
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.
|
|
89
|
+
|
|
90
|
+
## Agent prompt loop
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
frontpage prompt "make the hero shorter"
|
|
94
|
+
frontpage runs wait # wait for the agent run to finish
|
|
95
|
+
# Review https://<subdomain>.frontpage.host — do not publish unless asked.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Mid-task continuation is automatic. There is **no** `runs continue` in v1.
|
|
99
|
+
|
|
100
|
+
## Content / JSON-native commands
|
|
101
|
+
|
|
102
|
+
These already return JSON bodies (wrap further with global `--json` where supported):
|
|
103
|
+
|
|
104
|
+
- `usage`, `analytics`, `seo`, `articles`, `settings`, `inbox` (default JSON)
|
|
105
|
+
- Mutate: `modules enable|disable|archive|config`, `extensions …`
|
|
106
|
+
|
|
107
|
+
Click-edit (exact single occurrence), **not** file push:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
frontpage edit --path / --old "exact old" --new "exact new"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`edit` also updates **staging**, same as push. Still do not publish unless asked.
|
|
114
|
+
|
|
115
|
+
## History / revert
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
frontpage history --json
|
|
119
|
+
frontpage revert <40-char-sha> --yes # required --yes when non-TTY; only if user asked
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- `S` = staging tip, `P` = production tip.
|
|
123
|
+
- Revert creates a **new** commit whose tree matches the target (linear restore) on the site branch (staging).
|
|
124
|
+
- Full 40-char SHA only. Refused while an agent run is active.
|
|
125
|
+
|
|
126
|
+
## Exit codes — what to do next
|
|
127
|
+
|
|
128
|
+
| Code | Meaning | Next step |
|
|
129
|
+
|------|---------|-----------|
|
|
130
|
+
| 0 | Success | Continue |
|
|
131
|
+
| 2 | Usage / budget / rate-limit / unavailable / failed run wait | Fix args, wait, or check `usage` |
|
|
132
|
+
| 3 | Conflict (CAS tip moved, active run, cancel-while-deploying) | `status` / `pull` / `runs wait`; retry |
|
|
133
|
+
| 4 | Authn/authz **or** site not found (404 often hides existence) | Check key, membership, subdomain |
|
|
134
|
+
| 5 | Unexpected server/network / API disabled | Retry; verify `PUBLIC_CLI_ENABLED` / `FRONTPAGE_API_BASE` |
|
|
135
|
+
|
|
136
|
+
Global machine envelope (`--json`):
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{ "ok": true, "command": "status", "data": { } }
|
|
140
|
+
{ "ok": false, "command": "push", "error": { "message": "…", "code": "conflict", "httpStatus": 409, "exitCode": 3 } }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Failures with `--json` print the envelope on **stdout** (still use the process exit code).
|
|
144
|
+
|
|
145
|
+
## Global flags
|
|
146
|
+
|
|
147
|
+
| Flag | Meaning |
|
|
148
|
+
|------|---------|
|
|
149
|
+
| `--json` | Stable JSON envelope for supported commands |
|
|
150
|
+
| `--quiet` | Suppress progress spinners / chatter |
|
|
151
|
+
| `--site <slug>` | Subdomain override (most site commands) |
|
|
152
|
+
| `--yes` | Confirm destructive ops (`unpublish`, `revert`) when non-TTY |
|
|
153
|
+
| `--help` / `-h` | Per-command help (alias of `frontpage help <cmd>`) |
|
|
154
|
+
|
|
155
|
+
## Destructive ops — defaults for agents
|
|
156
|
+
|
|
157
|
+
**Never** run unless the user explicitly asked (or their workflow clearly requires it):
|
|
158
|
+
|
|
159
|
+
- `frontpage publish` (promotes staging → **production** / custom domain)
|
|
160
|
+
- `frontpage unpublish` (takes production offline) — requires `--yes` when non-TTY
|
|
161
|
+
- `frontpage revert <sha>` — requires `--yes` when non-TTY
|
|
162
|
+
- `frontpage inbox … --mark-read` (side-effecting)
|
|
163
|
+
- Broad `settings` mutations or mass `edit` without a clear request
|
|
164
|
+
|
|
165
|
+
Safe default after finishing work: summarize what changed and give the staging URL for review.
|
|
166
|
+
|
|
167
|
+
## Other nuances agents often miss
|
|
168
|
+
|
|
169
|
+
1. **CLI site id ≠ public domain.** Target `asimov-press`, not `asimov-press.frontpage.host` or a custom domain.
|
|
170
|
+
2. **Keys are account-wide.** One key can reach every site the user can access.
|
|
171
|
+
3. **404 → exit 4** often means “no access / not found,” not only a bad password.
|
|
172
|
+
4. **Allowlist only.** If a file is not under `src/`, `public/`, or `content/`, the CLI will not sync it. Some scaffold files are platform-owned and refuse CLI read/write.
|
|
173
|
+
5. **Full-tree push for allowlisted roots.** Local allowlisted files become the intended remote tree for those roots on push.
|
|
174
|
+
6. **Active-run guard.** Push/revert refuse while an editor agent run is active (exit 3).
|
|
175
|
+
7. **CAS.** Tip moved since pull → exit 3; pull and retry.
|
|
176
|
+
8. **No invented v1 commands:** `domains*`, `billing*`, `members*`, `suggestions*`, `sites delete|restore`, `runs continue`, MCP, git remotes / clone URLs.
|
|
177
|
+
9. **`deploy wait` ≠ publish.** Optional build wait only; staging visibility is the subdomain.
|
|
178
|
+
10. **Honesty on stubs.** `open --prod` is currently a no-op stub (still opens the subdomain).
|
|
179
|
+
|
|
180
|
+
## Isolation (hard fence)
|
|
181
|
+
|
|
182
|
+
- API never returns another site’s branch, GitHub tokens, or clone URLs.
|
|
183
|
+
- Site targeting is subdomain → `site/<slug>` only.
|
|
184
|
+
- See `frontpage help isolation`.
|
|
185
|
+
|
|
186
|
+
## Canonical introspection
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
frontpage help
|
|
190
|
+
frontpage help --json
|
|
191
|
+
frontpage help sync
|
|
192
|
+
frontpage help agent
|
|
193
|
+
frontpage doctor --json
|
|
194
|
+
```
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frontpage-host
|
|
2
|
+
|
|
3
|
+
Frontpage CLI — manage your site from the terminal.
|
|
4
|
+
|
|
5
|
+
**npm package:** `frontpage-host`
|
|
6
|
+
**Binary:** `frontpage`
|
|
7
|
+
**Do not install** the unrelated npm package named `frontpage`.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i -g frontpage-host
|
|
13
|
+
# or one-shot:
|
|
14
|
+
npx frontpage-host help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Node.js ≥ 20.
|
|
18
|
+
|
|
19
|
+
## 60-second path
|
|
20
|
+
|
|
21
|
+
1. In the Frontpage editor: profile menu → **Developers** → create an API key (`fp_live_…`).
|
|
22
|
+
2. `frontpage login` (or `frontpage login "$FRONTPAGE_API_KEY"`).
|
|
23
|
+
3. `frontpage sites` — note the **subdomain** (not a custom domain).
|
|
24
|
+
4. `mkdir my-site && cd my-site && frontpage use <subdomain>`
|
|
25
|
+
5. `frontpage pull` → edit files under `src/`, `public/`, or `content/`
|
|
26
|
+
6. `frontpage push --message "describe the change"`
|
|
27
|
+
7. Review staging at `https://<subdomain>.frontpage.host` (no separate staging-deploy step).
|
|
28
|
+
8. `frontpage publish` only when you intentionally promote to production.
|
|
29
|
+
|
|
30
|
+
## Environment
|
|
31
|
+
|
|
32
|
+
| Variable | Purpose |
|
|
33
|
+
|----------|---------|
|
|
34
|
+
| `FRONTPAGE_API_KEY` | Bearer key (preferred in CI; overrides keychain) |
|
|
35
|
+
| `FRONTPAGE_API_BASE` | API origin (default `https://app.frontpage.host`) |
|
|
36
|
+
|
|
37
|
+
## Help (source of truth in the binary)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
frontpage help # catalog + quickstart
|
|
41
|
+
frontpage help quickstart
|
|
42
|
+
frontpage help auth
|
|
43
|
+
frontpage help sync
|
|
44
|
+
frontpage help agent
|
|
45
|
+
frontpage help content
|
|
46
|
+
frontpage help history
|
|
47
|
+
frontpage help isolation
|
|
48
|
+
frontpage help exit-codes
|
|
49
|
+
frontpage help push # per-command details
|
|
50
|
+
frontpage push --help # same
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Machine-readable: `frontpage help --json` and global `--json` / `--quiet` on commands.
|
|
54
|
+
|
|
55
|
+
## Agents
|
|
56
|
+
|
|
57
|
+
See [AGENTS.md](./AGENTS.md) and [llms.txt](./llms.txt) in this package.
|
package/dist/bin.js
CHANGED
|
@@ -33,13 +33,21 @@ import { runHistory } from "./commands/history.js";
|
|
|
33
33
|
import { runRevert } from "./commands/revert.js";
|
|
34
34
|
import { runCompletion } from "./commands/completion.js";
|
|
35
35
|
import { EXIT_USAGE, EXIT_UNEXPECTED } from "./http/client.js";
|
|
36
|
+
import { parseGlobalFlags } from "./flags.js";
|
|
37
|
+
import { setRuntimeFlags, isJson } from "./runtime.js";
|
|
36
38
|
async function main() {
|
|
37
|
-
const
|
|
39
|
+
const parsed = parseGlobalFlags(process.argv.slice(2));
|
|
40
|
+
setRuntimeFlags({ json: parsed.json, quiet: parsed.quiet, yes: parsed.yes });
|
|
41
|
+
const argv = parsed.args;
|
|
38
42
|
const cmd = argv[0];
|
|
39
43
|
const args = argv.slice(1);
|
|
40
|
-
|
|
44
|
+
// Global or per-command help aliases (before site resolution).
|
|
45
|
+
if (!cmd || cmd === "help") {
|
|
41
46
|
return runHelp(cmd === "help" ? args : []);
|
|
42
47
|
}
|
|
48
|
+
if (parsed.wantsHelp) {
|
|
49
|
+
return runHelp([cmd, ...args]);
|
|
50
|
+
}
|
|
43
51
|
switch (cmd) {
|
|
44
52
|
case "login":
|
|
45
53
|
return runLogin(args);
|
|
@@ -109,8 +117,17 @@ async function main() {
|
|
|
109
117
|
case "completion":
|
|
110
118
|
return runCompletion(args);
|
|
111
119
|
default:
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
if (isJson()) {
|
|
121
|
+
console.log(JSON.stringify({
|
|
122
|
+
ok: false,
|
|
123
|
+
command: cmd,
|
|
124
|
+
error: { message: `Unknown command: ${cmd}`, exitCode: EXIT_USAGE },
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.error(`Unknown command: ${cmd}`);
|
|
129
|
+
console.error("Run: frontpage help");
|
|
130
|
+
}
|
|
114
131
|
return EXIT_USAGE;
|
|
115
132
|
}
|
|
116
133
|
}
|
|
@@ -119,6 +136,16 @@ main()
|
|
|
119
136
|
process.exitCode = code;
|
|
120
137
|
})
|
|
121
138
|
.catch((err) => {
|
|
122
|
-
|
|
139
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
140
|
+
if (isJson()) {
|
|
141
|
+
console.log(JSON.stringify({
|
|
142
|
+
ok: false,
|
|
143
|
+
command: "frontpage",
|
|
144
|
+
error: { message, exitCode: EXIT_UNEXPECTED },
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
console.error(message);
|
|
149
|
+
}
|
|
123
150
|
process.exitCode = EXIT_UNEXPECTED;
|
|
124
151
|
});
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
|
-
import { EXIT_OK, EXIT_USAGE,
|
|
2
|
+
import { EXIT_OK, EXIT_USAGE, v1Get } from "../http/client.js";
|
|
3
|
+
import { emitErr, emitOk, emitUsage, progress } from "../runtime.js";
|
|
4
|
+
import { isJson } from "../runtime.js";
|
|
3
5
|
/** `frontpage deploy wait` — wait until no run is in deploying/queued/running. */
|
|
4
6
|
export async function runDeploy(args) {
|
|
5
7
|
try {
|
|
6
8
|
const sub = args[0];
|
|
7
9
|
if (sub !== "wait") {
|
|
8
|
-
|
|
9
|
-
return EXIT_USAGE;
|
|
10
|
+
return emitUsage("deploy", "Usage: frontpage deploy wait [--site <slug>]", EXIT_USAGE);
|
|
10
11
|
}
|
|
11
12
|
const { slug } = await resolveSlug(args.slice(1));
|
|
12
13
|
const started = Date.now();
|
|
@@ -15,18 +16,26 @@ export async function runDeploy(args) {
|
|
|
15
16
|
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs`);
|
|
16
17
|
const busy = (data.active ?? []).filter((r) => ["queued", "running", "deploying"].includes(r.status));
|
|
17
18
|
if (busy.length === 0) {
|
|
18
|
-
|
|
19
|
+
if (isJson()) {
|
|
20
|
+
emitOk("deploy", {
|
|
21
|
+
slug,
|
|
22
|
+
waited: true,
|
|
23
|
+
run: data.run,
|
|
24
|
+
status: data.run?.status ?? "idle",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.log(data.run ? `${data.run.id} ${data.run.status}` : "No active deploy.");
|
|
29
|
+
}
|
|
19
30
|
return EXIT_OK;
|
|
20
31
|
}
|
|
21
32
|
const head = busy.find((r) => r.status === "deploying") ?? busy[0];
|
|
22
|
-
|
|
33
|
+
progress(`\r${head.status} ${head.statusMessage ?? ""} `);
|
|
23
34
|
await new Promise((r) => setTimeout(r, 2000));
|
|
24
35
|
}
|
|
25
|
-
|
|
26
|
-
return EXIT_USAGE;
|
|
36
|
+
return emitUsage("deploy", "Timed out waiting for deploy.", EXIT_USAGE);
|
|
27
37
|
}
|
|
28
38
|
catch (err) {
|
|
29
|
-
|
|
30
|
-
return exitCodeForError(err);
|
|
39
|
+
return emitErr("deploy", err);
|
|
31
40
|
}
|
|
32
41
|
}
|
package/dist/commands/diff.js
CHANGED
|
@@ -2,7 +2,8 @@ import { readFile, readdir, stat } from "node:fs/promises";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { join, relative, sep } from "node:path";
|
|
4
4
|
import { resolveSlug } from "../site.js";
|
|
5
|
-
import { EXIT_OK, EXIT_USAGE,
|
|
5
|
+
import { EXIT_OK, EXIT_USAGE, v1Get } from "../http/client.js";
|
|
6
|
+
import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
|
|
6
7
|
const ROOTS = ["src", "public", "content"];
|
|
7
8
|
async function walkLocal(cwd) {
|
|
8
9
|
const map = new Map();
|
|
@@ -47,47 +48,56 @@ export async function runDiff(args) {
|
|
|
47
48
|
const local = await walkLocal(cwd);
|
|
48
49
|
const listing = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/files`);
|
|
49
50
|
const remotePaths = new Set(listing.files.map((f) => f.path));
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const added = [];
|
|
52
|
+
const modified = [];
|
|
53
|
+
const deleted = [];
|
|
53
54
|
for (const path of [...local.keys()].sort()) {
|
|
54
55
|
if (!remotePaths.has(path)) {
|
|
55
|
-
|
|
56
|
-
onlyLocal++;
|
|
56
|
+
added.push(path);
|
|
57
57
|
continue;
|
|
58
58
|
}
|
|
59
59
|
const encPath = path.split("/").map(encodeURIComponent).join("/");
|
|
60
60
|
const remote = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/files/${encPath}`);
|
|
61
61
|
const rh = remoteHash(remote.file);
|
|
62
62
|
const lh = local.get(path);
|
|
63
|
-
if (rh !== lh)
|
|
64
|
-
|
|
65
|
-
changed++;
|
|
66
|
-
}
|
|
63
|
+
if (rh !== lh)
|
|
64
|
+
modified.push(path);
|
|
67
65
|
}
|
|
68
66
|
for (const f of listing.files) {
|
|
69
|
-
if (!local.has(f.path))
|
|
70
|
-
|
|
71
|
-
onlyRemote++;
|
|
72
|
-
}
|
|
67
|
+
if (!local.has(f.path))
|
|
68
|
+
deleted.push(f.path);
|
|
73
69
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
70
|
+
const dirty = added.length + modified.length + deleted.length > 0;
|
|
71
|
+
void (await stat(cwd).catch(() => null));
|
|
72
|
+
if (isJson()) {
|
|
73
|
+
emitOk("diff", {
|
|
74
|
+
slug,
|
|
75
|
+
baseCommitSha: listing.baseCommitSha,
|
|
76
|
+
dirty,
|
|
77
|
+
added,
|
|
78
|
+
modified,
|
|
79
|
+
deleted,
|
|
80
|
+
});
|
|
81
|
+
return EXIT_OK;
|
|
82
|
+
}
|
|
83
|
+
for (const path of added)
|
|
84
|
+
console.log(`A ${path}`);
|
|
85
|
+
for (const path of modified)
|
|
86
|
+
console.log(`M ${path}`);
|
|
87
|
+
for (const path of deleted)
|
|
88
|
+
console.log(`D ${path}`);
|
|
89
|
+
if (!dirty) {
|
|
76
90
|
console.log("No differences.");
|
|
77
91
|
}
|
|
78
92
|
else {
|
|
79
|
-
console.error(`# ${
|
|
93
|
+
console.error(`# ${added.length + modified.length + deleted.length} difference(s) · tip ${(listing.baseCommitSha ?? "").slice(0, 7) || "?"} · A=local-only M=modified D=remote-only`);
|
|
80
94
|
}
|
|
81
|
-
// Ensure cwd has the tree (optional UX check)
|
|
82
|
-
void (await stat(cwd).catch(() => null));
|
|
83
95
|
return EXIT_OK;
|
|
84
96
|
}
|
|
85
97
|
catch (err) {
|
|
86
98
|
if (err instanceof Error && /No site selected/.test(err.message)) {
|
|
87
|
-
|
|
88
|
-
return EXIT_USAGE;
|
|
99
|
+
return emitUsage("diff", err.message, EXIT_USAGE);
|
|
89
100
|
}
|
|
90
|
-
|
|
91
|
-
return exitCodeForError(err);
|
|
101
|
+
return emitErr("diff", err);
|
|
92
102
|
}
|
|
93
103
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,26 +1,51 @@
|
|
|
1
1
|
import { getApiKey, maskKey } from "../auth/keychain.js";
|
|
2
|
-
import { apiBase, EXIT_OK, EXIT_AUTH,
|
|
2
|
+
import { apiBase, EXIT_OK, EXIT_AUTH, exitCodeForError, v1Get } from "../http/client.js";
|
|
3
3
|
import { readState } from "../state/frontpage-state.js";
|
|
4
|
+
import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
|
|
4
5
|
export async function runDoctor() {
|
|
5
|
-
console.log(`API base: ${apiBase()}`);
|
|
6
6
|
const key = await getApiKey();
|
|
7
|
+
const state = await readState();
|
|
8
|
+
const base = apiBase();
|
|
7
9
|
if (!key) {
|
|
10
|
+
if (isJson()) {
|
|
11
|
+
return emitUsage("doctor", "No API key. Run: frontpage login", EXIT_AUTH);
|
|
12
|
+
}
|
|
8
13
|
console.error("No API key. Run: frontpage login");
|
|
9
14
|
return EXIT_AUTH;
|
|
10
15
|
}
|
|
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
16
|
try {
|
|
15
17
|
const me = await v1Get("/api/v1/me");
|
|
16
|
-
console.log(`whoami: ok (${me.email ?? me.userId})`);
|
|
17
18
|
const sites = await v1Get("/api/v1/sites");
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const data = {
|
|
20
|
+
apiBase: base,
|
|
21
|
+
apiKeyMasked: maskKey(key),
|
|
22
|
+
folderSite: state.slug ?? null,
|
|
23
|
+
baseCommitSha: state.baseCommitSha ?? null,
|
|
24
|
+
whoami: { email: me.email ?? null, userId: me.userId },
|
|
25
|
+
siteCount: sites.sites.length,
|
|
26
|
+
ok: true,
|
|
27
|
+
};
|
|
28
|
+
if (isJson()) {
|
|
29
|
+
emitOk("doctor", data);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
console.log(`API base: ${base}`);
|
|
33
|
+
console.log(`API key: ${maskKey(key)} (never prints the full secret)`);
|
|
34
|
+
console.log(`Folder site: ${state.slug ?? "(not set — run frontpage use <subdomain>)"}`);
|
|
35
|
+
console.log(`whoami: ok (${me.email ?? me.userId})`);
|
|
36
|
+
console.log(`sites: ok (${sites.sites.length} site(s))`);
|
|
37
|
+
console.log("doctor: all checks passed");
|
|
38
|
+
}
|
|
20
39
|
return EXIT_OK;
|
|
21
40
|
}
|
|
22
41
|
catch (err) {
|
|
42
|
+
if (isJson()) {
|
|
43
|
+
return emitErr("doctor", err);
|
|
44
|
+
}
|
|
45
|
+
console.log(`API base: ${base}`);
|
|
46
|
+
console.log(`API key: ${maskKey(key)} (never prints the full secret)`);
|
|
47
|
+
console.log(`Folder site: ${state.slug ?? "(not set — run frontpage use <subdomain>)"}`);
|
|
23
48
|
console.error(err instanceof Error ? err.message : String(err));
|
|
24
|
-
return
|
|
49
|
+
return exitCodeForError(err);
|
|
25
50
|
}
|
|
26
51
|
}
|
package/dist/commands/help.js
CHANGED
|
@@ -1,14 +1,77 @@
|
|
|
1
|
-
import { COMMANDS, TOPICS, findCommand } from "../registry.js";
|
|
1
|
+
import { COMMANDS, TOPIC_NAMES, TOPICS, findCommand } from "../registry.js";
|
|
2
|
+
import { isJson, emitOk } from "../runtime.js";
|
|
3
|
+
function printCommandHelp(cmd) {
|
|
4
|
+
console.log(`${cmd.name} — ${cmd.summary}`);
|
|
5
|
+
if (cmd.usage)
|
|
6
|
+
console.log(`Usage: ${cmd.usage}`);
|
|
7
|
+
if (cmd.flags?.length) {
|
|
8
|
+
console.log("\nFlags / notes:");
|
|
9
|
+
for (const f of cmd.flags)
|
|
10
|
+
console.log(` - ${f}`);
|
|
11
|
+
}
|
|
12
|
+
if (cmd.examples?.length) {
|
|
13
|
+
console.log("\nExamples:");
|
|
14
|
+
for (const e of cmd.examples)
|
|
15
|
+
console.log(` ${e}`);
|
|
16
|
+
}
|
|
17
|
+
if (cmd.dangers?.length) {
|
|
18
|
+
console.log("\nDanger:");
|
|
19
|
+
for (const d of cmd.dangers)
|
|
20
|
+
console.log(` ! ${d}`);
|
|
21
|
+
}
|
|
22
|
+
if (cmd.seeAlso?.length) {
|
|
23
|
+
console.log(`\nSee also: ${cmd.seeAlso.join(", ")}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
2
26
|
export function runHelp(args) {
|
|
3
|
-
const
|
|
27
|
+
const filtered = args.filter((a) => a !== "--json" && a !== "--quiet" && a !== "-q");
|
|
28
|
+
const topic = filtered[0]?.trim();
|
|
29
|
+
if (isJson()) {
|
|
30
|
+
if (!topic) {
|
|
31
|
+
emitOk("help", {
|
|
32
|
+
commands: COMMANDS.map((c) => ({
|
|
33
|
+
name: c.name,
|
|
34
|
+
summary: c.summary,
|
|
35
|
+
usage: c.usage,
|
|
36
|
+
topics: c.topics,
|
|
37
|
+
examples: c.examples,
|
|
38
|
+
flags: c.flags,
|
|
39
|
+
dangers: c.dangers,
|
|
40
|
+
seeAlso: c.seeAlso,
|
|
41
|
+
})),
|
|
42
|
+
topics: TOPIC_NAMES,
|
|
43
|
+
topicBodies: TOPICS,
|
|
44
|
+
});
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
if (TOPICS[topic]) {
|
|
48
|
+
emitOk("help", { topic, body: TOPICS[topic] });
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
const cmd = findCommand(topic);
|
|
52
|
+
if (cmd) {
|
|
53
|
+
emitOk("help", { command: cmd });
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
console.log(JSON.stringify({
|
|
57
|
+
ok: false,
|
|
58
|
+
command: "help",
|
|
59
|
+
error: { message: `Unknown help topic: ${topic}`, exitCode: 2 },
|
|
60
|
+
}));
|
|
61
|
+
return 2;
|
|
62
|
+
}
|
|
4
63
|
if (!topic) {
|
|
5
64
|
console.log("Frontpage CLI\n");
|
|
6
|
-
console.log("
|
|
7
|
-
console.log("
|
|
65
|
+
console.log("Package: frontpage-host · Binary: frontpage");
|
|
66
|
+
console.log("Do not install the unrelated npm package named \"frontpage\".\n");
|
|
67
|
+
console.log(TOPICS.quickstart.split("\n").slice(0, 12).join("\n"));
|
|
68
|
+
console.log("\nCommands:");
|
|
8
69
|
for (const c of COMMANDS) {
|
|
9
|
-
console.log(` ${c.name.padEnd(
|
|
70
|
+
console.log(` ${c.name.padEnd(12)} ${c.summary}`);
|
|
10
71
|
}
|
|
11
|
-
console.log(
|
|
72
|
+
console.log(`\nTopics: frontpage help ${TOPIC_NAMES.join(" | frontpage help ")}`);
|
|
73
|
+
console.log("Global: --json --quiet --yes --site <slug> --help");
|
|
74
|
+
console.log("Agents: see AGENTS.md in the npm package");
|
|
12
75
|
return 0;
|
|
13
76
|
}
|
|
14
77
|
if (TOPICS[topic]) {
|
|
@@ -17,9 +80,7 @@ export function runHelp(args) {
|
|
|
17
80
|
}
|
|
18
81
|
const cmd = findCommand(topic);
|
|
19
82
|
if (cmd) {
|
|
20
|
-
|
|
21
|
-
if (cmd.usage)
|
|
22
|
-
console.log(`Usage: ${cmd.usage}`);
|
|
83
|
+
printCommandHelp(cmd);
|
|
23
84
|
return 0;
|
|
24
85
|
}
|
|
25
86
|
console.error(`Unknown help topic: ${topic}`);
|
package/dist/commands/history.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
|
-
import { EXIT_OK, EXIT_USAGE,
|
|
2
|
+
import { EXIT_OK, EXIT_USAGE, v1Get } from "../http/client.js";
|
|
3
|
+
import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
|
|
3
4
|
export async function runHistory(args) {
|
|
4
5
|
try {
|
|
5
6
|
const { slug, rest } = await resolveSlug(args);
|
|
@@ -23,13 +24,13 @@ export async function runHistory(args) {
|
|
|
23
24
|
page = a.slice("--page=".length);
|
|
24
25
|
continue;
|
|
25
26
|
}
|
|
26
|
-
if (a === "--help" || a === "-h") {
|
|
27
|
-
console.error("Usage: frontpage history [--limit N] [--page N] [--site <slug>]");
|
|
28
|
-
return EXIT_USAGE;
|
|
29
|
-
}
|
|
30
27
|
}
|
|
31
28
|
const qs = new URLSearchParams({ limit, page });
|
|
32
29
|
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/history?${qs}`);
|
|
30
|
+
if (isJson()) {
|
|
31
|
+
emitOk("history", { slug, ...data });
|
|
32
|
+
return EXIT_OK;
|
|
33
|
+
}
|
|
33
34
|
for (const c of data.commits) {
|
|
34
35
|
const mark = data.stagingCommit === c.sha ? "S" : data.productionCommit === c.sha ? "P" : " ";
|
|
35
36
|
const who = c.actorEmail ?? c.actorKind ?? "";
|
|
@@ -43,7 +44,9 @@ export async function runHistory(args) {
|
|
|
43
44
|
return EXIT_OK;
|
|
44
45
|
}
|
|
45
46
|
catch (err) {
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
if (err instanceof Error && /Usage:/.test(err.message)) {
|
|
48
|
+
return emitUsage("history", err.message, EXIT_USAGE);
|
|
49
|
+
}
|
|
50
|
+
return emitErr("history", err);
|
|
48
51
|
}
|
|
49
52
|
}
|
package/dist/commands/ls.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
|
-
import { EXIT_OK,
|
|
2
|
+
import { EXIT_OK, v1Get } from "../http/client.js";
|
|
3
|
+
import { emitErr, emitOk, isJson } from "../runtime.js";
|
|
3
4
|
export async function runLs(args) {
|
|
4
5
|
try {
|
|
5
6
|
const { slug, rest } = await resolveSlug(args);
|
|
6
7
|
const prefix = rest[0];
|
|
7
8
|
const q = prefix ? `?prefix=${encodeURIComponent(prefix)}` : "";
|
|
8
9
|
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/files${q}`);
|
|
10
|
+
if (isJson()) {
|
|
11
|
+
emitOk("ls", { ...data, prefix: prefix ?? null });
|
|
12
|
+
return EXIT_OK;
|
|
13
|
+
}
|
|
9
14
|
for (const f of data.files) {
|
|
10
15
|
console.log(`${String(f.bytes).padStart(10)} ${f.path}`);
|
|
11
16
|
}
|
|
@@ -13,7 +18,6 @@ export async function runLs(args) {
|
|
|
13
18
|
return EXIT_OK;
|
|
14
19
|
}
|
|
15
20
|
catch (err) {
|
|
16
|
-
|
|
17
|
-
return exitCodeForError(err);
|
|
21
|
+
return emitErr("ls", err);
|
|
18
22
|
}
|
|
19
23
|
}
|