@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 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 all form submissions for the current project |
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.0.8",
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": {
@@ -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) {
@@ -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 db
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
- console.error('Failed to list forms:', err.message);
26
- process.exit(1);
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) => ({ ...f, submission_count: counts[f.id] || 0 }));
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.form_name,
88
+ formLabel(f),
63
89
  f.page_url,
64
90
  f.is_footer ? 'footer' : 'inline',
65
91
  String(counts[f.id] || 0),