@micropage-sh/cli 2.0.8 → 2.6.1
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 +61 -4
- package/bin/micropage.js +15 -0
- package/package.json +3 -2
- package/src/commands/builds.js +8 -1
- package/src/commands/forms.js +37 -11
- package/src/commands/posts.js +646 -0
- package/src/commands/projects.js +1 -0
- package/src/commands/submissions.js +50 -25
- package/src/index.js +45 -3
- package/src/parser.js +16 -0
- package/src/posts-assets.js +211 -0
- package/src/supabase.js +36 -3
- package/templates/PROJECT_AGENT.template.md +53 -4
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ brew install micropage
|
|
|
19
19
|
### npm
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npm install -g micropage
|
|
22
|
+
npm install -g @micropage-sh/cli
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
## Authentication
|
|
@@ -63,6 +63,40 @@ micropage copy-link
|
|
|
63
63
|
|
|
64
64
|
When you create a project without `-d`, the API assigns `projects.domain` as the initial host for your site (for example `my-site.micropage.sh`). You can optionally attach a custom domain (such as `www.example.com`) in the web editor; when present, the CLI shows that custom domain as the site URL (otherwise the default host).
|
|
65
65
|
|
|
66
|
+
### Authoring posts
|
|
67
|
+
|
|
68
|
+
Posts live in the project's `posts/` folder as Markdown files with YAML front-matter:
|
|
69
|
+
|
|
70
|
+
```markdown
|
|
71
|
+
---
|
|
72
|
+
title: Hello, world
|
|
73
|
+
slug: hello
|
|
74
|
+
description: A short summary for the archive, meta description, and og tags.
|
|
75
|
+
visibility: listed
|
|
76
|
+
hero: ./hello.jpg
|
|
77
|
+
list: newsletter
|
|
78
|
+
subject: Hello, world!
|
|
79
|
+
preview: The first post on this site.
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
Body content goes here as standard Markdown.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`title` is required. `slug` defaults to the filename minus a leading `YYYY-MM-DD-` date prefix. `visibility` is `listed` (default, appears in the site's `/content` index) or `unlisted`. `list` names a newsletter form and is required to email the post on publish. Local image references in the body are auto-uploaded and rewritten to hosted URLs on `push`.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Save posts/hello.md as a draft
|
|
89
|
+
micropage posts push
|
|
90
|
+
|
|
91
|
+
# Publish it (and email the `list:` target, if set)
|
|
92
|
+
micropage posts publish hello
|
|
93
|
+
|
|
94
|
+
# Take it back down (stays as a draft)
|
|
95
|
+
micropage posts unpublish hello
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
See [`micropage posts`](#posts) below for the full command set.
|
|
99
|
+
|
|
66
100
|
## Commands reference
|
|
67
101
|
|
|
68
102
|
### Auth
|
|
@@ -116,6 +150,19 @@ When you create a new project, the CLI also scaffolds:
|
|
|
116
150
|
| `micropage copy-link` | Copy the live URL to clipboard |
|
|
117
151
|
| `micropage open-pricing` | Open the pricing page in the browser |
|
|
118
152
|
|
|
153
|
+
### Posts
|
|
154
|
+
|
|
155
|
+
| Command | Description |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `micropage posts push` | Save local `posts/*.md` files as post drafts (or update already-published posts, which go live immediately) |
|
|
158
|
+
| `micropage posts publish [slug] [-w]` | Publish a post (or all local posts) to the web; sends the newsletter email if the post has a `list:`. Re-running re-sends the email. Auto-queues a rebuild of the site so the `/content` archive updates — no separate `micropage publish` needed. Use `-w` to stream the rebuild's deploy events until it's live. |
|
|
159
|
+
| `micropage posts unpublish <slug>` | Remove a post's `/content/<slug>` page; the post remains as a draft |
|
|
160
|
+
| `micropage posts pull` | Pull remote posts down to local `posts/*.md` files |
|
|
161
|
+
| `micropage posts list` | List the project's posts (slug, title, visibility, published/draft, emailed, send status, created). "Send status" is the newsletter send lifecycle and shows only for email posts — it does not reflect deploy state. |
|
|
162
|
+
| `micropage posts rm <slug>` | Delete a post entirely (remote) |
|
|
163
|
+
|
|
164
|
+
**Note:** `micropage posts publish` publishes a single *post* and automatically rebuilds the site so the post appears on the `/content` archive — you do **not** need to run `micropage publish` afterward. `micropage publish` is for deploying changes to the *site* content (`.page` files) itself.
|
|
165
|
+
|
|
119
166
|
### Files
|
|
120
167
|
|
|
121
168
|
| Command | Description |
|
|
@@ -128,9 +175,9 @@ When you create a new project, the CLI also scaffolds:
|
|
|
128
175
|
|
|
129
176
|
| Command | Description |
|
|
130
177
|
|---|---|
|
|
131
|
-
| `micropage submissions list [--json]` | List form submissions for the project |
|
|
132
|
-
| `micropage submissions show <id> [--json]` | Show a single submission in detail |
|
|
133
|
-
| `micropage submissions export [--format csv|json] [-o file]` | Export
|
|
178
|
+
| `micropage submissions list [--json] [--spam]` | List form submissions for the project. Spam is excluded; `--spam` lists only submissions flagged as spam, with the reason |
|
|
179
|
+
| `micropage submissions show <id> [--json]` | Show a single submission in detail, including a `Spam:` line with the reason when it was flagged |
|
|
180
|
+
| `micropage submissions export [--format csv|json] [-o file] [--spam]` | Export form submissions for the current project. Spam is excluded; `--spam` exports only flagged submissions (default file `submissions-spam.<format>`, CSV gets a trailing `spam_reason` column) |
|
|
134
181
|
|
|
135
182
|
**Examples:**
|
|
136
183
|
|
|
@@ -152,6 +199,12 @@ When you create a new project, the CLI also scaffolds:
|
|
|
152
199
|
micropage submissions export --format json --output submissions.json
|
|
153
200
|
```
|
|
154
201
|
|
|
202
|
+
- Export only submissions flagged as spam:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
micropage submissions export --spam
|
|
206
|
+
```
|
|
207
|
+
|
|
155
208
|
## Local content model
|
|
156
209
|
|
|
157
210
|
The CLI uses one or more `*.page` files in the working directory as the source of truth for project content.
|
|
@@ -163,6 +216,10 @@ The CLI uses one or more `*.page` files in the working directory as the source o
|
|
|
163
216
|
|
|
164
217
|
**Pulling** always writes to `landing.page` only.
|
|
165
218
|
|
|
219
|
+
### `llms.txt`
|
|
220
|
+
|
|
221
|
+
If the project folder contains a root-level `llms.txt`, its contents are published verbatim and served at `/llms.txt` (a curated map of the site for LLM agents). Remove the local file and re-publish to take it down.
|
|
222
|
+
|
|
166
223
|
## Project configuration
|
|
167
224
|
|
|
168
225
|
Each project folder has a `.micropage/project.json` file that stores:
|
package/bin/micropage.js
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// Suppress Node's "buffer.File is an experimental feature" warning that fires
|
|
4
|
+
// when undici's FormData serializes a Blob upload. Stabilized in Node 22.
|
|
5
|
+
const origEmit = process.emit;
|
|
6
|
+
process.emit = function (name, data, ...rest) {
|
|
7
|
+
if (
|
|
8
|
+
name === 'warning' &&
|
|
9
|
+
data &&
|
|
10
|
+
data.name === 'ExperimentalWarning' &&
|
|
11
|
+
/buffer\.File/.test(String(data.message))
|
|
12
|
+
) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return origEmit.call(this, name, data, ...rest);
|
|
16
|
+
};
|
|
17
|
+
|
|
3
18
|
require('../src/index.js');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@micropage-sh/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "CLI for micropage.sh - create, sync, and publish microsites",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"test": "node -e \"require('./src/index.js')\"",
|
|
16
|
+
"test": "node -e \"require('./src/index.js')\"; node --test test/*.test.js test/*_test.mjs",
|
|
17
17
|
"build:bin": "pkg . --targets node18-linux-x64,node18-macos-x64,node18-macos-arm64 --output dist/micropage",
|
|
18
18
|
"build:linux": "pkg . --target node18-linux-x64 --output dist/micropage-linux-amd64",
|
|
19
19
|
"build:macos-amd64": "pkg . --target node18-macos-x64 --output dist/micropage-darwin-amd64",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"commander": "^12.0.0",
|
|
60
|
+
"gray-matter": "^4.0.3",
|
|
60
61
|
"open": "^10.1.0"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
package/src/commands/builds.js
CHANGED
|
@@ -14,7 +14,7 @@ const {
|
|
|
14
14
|
uploadAssetsWithToken,
|
|
15
15
|
} = require('../supabase');
|
|
16
16
|
const { getProjectConfig, setProjectConfig } = require('../auth');
|
|
17
|
-
const { readPageFilesFromDir } = require('../parser');
|
|
17
|
+
const { readPageFilesFromDir, readLlmsTxtFromDir } = require('../parser');
|
|
18
18
|
const { BUILD_COMPILER_URL } = require('../config');
|
|
19
19
|
const { formatTable, formatDate } = require('../utils');
|
|
20
20
|
|
|
@@ -167,6 +167,13 @@ async function push(options = {}) {
|
|
|
167
167
|
process.exit(1);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
const llmsTxt = readLlmsTxtFromDir(cwd);
|
|
171
|
+
if (llmsTxt != null) {
|
|
172
|
+
jsonContent.site = jsonContent.site || {};
|
|
173
|
+
jsonContent.site.llms_txt = llmsTxt;
|
|
174
|
+
console.log(`Including llms.txt (${Buffer.byteLength(llmsTxt, 'utf8')} bytes).`);
|
|
175
|
+
}
|
|
176
|
+
|
|
170
177
|
// Decide whether to update existing draft or create new build
|
|
171
178
|
let existingBuild = null;
|
|
172
179
|
if (config.buildId) {
|
package/src/commands/forms.js
CHANGED
|
@@ -4,6 +4,25 @@ const { db, handleAuthError } = require('../supabase');
|
|
|
4
4
|
const { getProjectConfig } = require('../auth');
|
|
5
5
|
const { formatTable, formatDate } = require('../utils');
|
|
6
6
|
|
|
7
|
+
const FORM_COLUMNS = 'id,form_name,page_url,is_footer,created_at';
|
|
8
|
+
|
|
9
|
+
/** Occurrence number among same-named forms on the same page, 1-based. */
|
|
10
|
+
function formLabel(form) {
|
|
11
|
+
const ordinal = Number(form.form_ordinal) || 0;
|
|
12
|
+
return ordinal > 0 ? `${form.form_name} (${ordinal + 1})` : form.form_name;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fetchForms(projectId, withOrdinal) {
|
|
16
|
+
const q = db
|
|
17
|
+
.from('forms')
|
|
18
|
+
.select(withOrdinal ? `${FORM_COLUMNS},form_ordinal` : FORM_COLUMNS)
|
|
19
|
+
.eq('project_id', projectId);
|
|
20
|
+
|
|
21
|
+
return withOrdinal
|
|
22
|
+
? q.order('page_url,form_ordinal', 'asc').get()
|
|
23
|
+
: q.order('created_at', 'asc').get();
|
|
24
|
+
}
|
|
25
|
+
|
|
7
26
|
async function list(options = {}) {
|
|
8
27
|
const cwd = process.cwd();
|
|
9
28
|
const config = getProjectConfig(cwd);
|
|
@@ -14,16 +33,18 @@ async function list(options = {}) {
|
|
|
14
33
|
|
|
15
34
|
let forms;
|
|
16
35
|
try {
|
|
17
|
-
forms = await
|
|
18
|
-
.from('forms')
|
|
19
|
-
.select('id,form_name,page_url,is_footer,created_at')
|
|
20
|
-
.eq('project_id', config.projectId)
|
|
21
|
-
.order('created_at', 'asc')
|
|
22
|
-
.get();
|
|
36
|
+
forms = await fetchForms(config.projectId, true);
|
|
23
37
|
} catch (err) {
|
|
24
38
|
handleAuthError(err);
|
|
25
|
-
|
|
26
|
-
|
|
39
|
+
// form_ordinal is newer than some deployed databases; selecting a column
|
|
40
|
+
// that does not exist fails the whole request, so retry the older shape.
|
|
41
|
+
try {
|
|
42
|
+
forms = await fetchForms(config.projectId, false);
|
|
43
|
+
} catch (retryErr) {
|
|
44
|
+
handleAuthError(retryErr);
|
|
45
|
+
console.error('Failed to list forms:', retryErr.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
27
48
|
}
|
|
28
49
|
|
|
29
50
|
if (!Array.isArray(forms) || forms.length === 0) {
|
|
@@ -31,13 +52,14 @@ async function list(options = {}) {
|
|
|
31
52
|
return;
|
|
32
53
|
}
|
|
33
54
|
|
|
34
|
-
// Count submissions per form_id
|
|
55
|
+
// Count non-spam submissions per form_id
|
|
35
56
|
let submissions;
|
|
36
57
|
try {
|
|
37
58
|
submissions = await db
|
|
38
59
|
.from('form_submissions')
|
|
39
60
|
.select('form_id')
|
|
40
61
|
.eq('project_id', config.projectId)
|
|
62
|
+
.is('flagged_at', 'null')
|
|
41
63
|
.get();
|
|
42
64
|
} catch (err) {
|
|
43
65
|
handleAuthError(err);
|
|
@@ -53,13 +75,17 @@ async function list(options = {}) {
|
|
|
53
75
|
}
|
|
54
76
|
|
|
55
77
|
if (options.json) {
|
|
56
|
-
const out = forms.map((f) => ({
|
|
78
|
+
const out = forms.map((f) => ({
|
|
79
|
+
...f,
|
|
80
|
+
form_ordinal: Number(f.form_ordinal) || 0,
|
|
81
|
+
submission_count: counts[f.id] || 0,
|
|
82
|
+
}));
|
|
57
83
|
console.log(JSON.stringify(out, null, 2));
|
|
58
84
|
return;
|
|
59
85
|
}
|
|
60
86
|
|
|
61
87
|
const rows = forms.map((f) => [
|
|
62
|
-
f
|
|
88
|
+
formLabel(f),
|
|
63
89
|
f.page_url,
|
|
64
90
|
f.is_footer ? 'footer' : 'inline',
|
|
65
91
|
String(counts[f.id] || 0),
|