@micropage-sh/cli 2.6.2 → 2.6.4
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 +7 -6
- package/package.json +2 -2
- package/src/commands/posts.js +84 -0
- package/src/index.js +4 -4
- package/templates/PROJECT_AGENT.template.md +1 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# micropage CLI
|
|
2
2
|
|
|
3
|
-
Command-line interface for [micropage.sh](https://
|
|
3
|
+
Command-line interface for [micropage.sh](https://micropage.sh): keep your `.page` files in git and create, edit, publish and manage your sites from the terminal.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
@@ -48,7 +48,7 @@ nano landing.page
|
|
|
48
48
|
# Push local content as a draft build
|
|
49
49
|
micropage push
|
|
50
50
|
|
|
51
|
-
# Publish the current draft (deploys
|
|
51
|
+
# Publish the current draft (deploys it live)
|
|
52
52
|
micropage publish
|
|
53
53
|
|
|
54
54
|
# Pull the latest remote build content to landing.page
|
|
@@ -73,6 +73,7 @@ Posts live in the project's `posts/` folder as Markdown files with YAML front-ma
|
|
|
73
73
|
---
|
|
74
74
|
title: Hello, world
|
|
75
75
|
slug: hello
|
|
76
|
+
date: 2026-07-07
|
|
76
77
|
description: A short summary for the archive, meta description, and og tags.
|
|
77
78
|
visibility: listed
|
|
78
79
|
hero: ./hello.jpg
|
|
@@ -84,7 +85,7 @@ preview: The first post on this site.
|
|
|
84
85
|
Body content goes here as standard Markdown.
|
|
85
86
|
```
|
|
86
87
|
|
|
87
|
-
`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`.
|
|
88
|
+
`title` is required. `slug` defaults to the filename minus a leading `YYYY-MM-DD-` date prefix. `date` is the post's public date: `YYYY-MM-DD` (midnight UTC) or a full ISO timestamp; it sets the published date of a live post, and a draft keeps it until its first publish. Omit it to use the publish time. Future dates are rejected (scheduling isn't supported); `posts pull` writes it back as the UTC day for published posts. `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`.
|
|
88
89
|
|
|
89
90
|
```bash
|
|
90
91
|
# Save posts/hello.md as a draft
|
|
@@ -115,7 +116,7 @@ See [`micropage posts`](#posts) below for the full command set.
|
|
|
115
116
|
|---|---|
|
|
116
117
|
| `micropage projects list [--json]` | List your projects |
|
|
117
118
|
| `micropage projects show [id] [--json]` | Show project + latest build details |
|
|
118
|
-
| `micropage projects create <name> [-d domain]` | Create a project and init local folder. Omit `-d` to let the API assign `{slugified-name}-{6 hex}` (unique
|
|
119
|
+
| `micropage projects create <name> [-d domain]` | Create a project and init local folder. Omit `-d` to let the API assign `{slugified-name}-{6 hex}` (unique `<slug>.micropage.sh` label, max 58 chars). Use `-d` only to override the slug. |
|
|
119
120
|
| `micropage projects fetch <uuid\|domain>` | Fetch an existing project and init local folder (includes storage → `./assets` sync) |
|
|
120
121
|
| `micropage projects pull` | Pull latest build raw content → `landing.page`, then sync `./assets` with project storage (1:1) |
|
|
121
122
|
| `micropage projects delete [-y]` | Delete project (remote + local `.micropage/`) |
|
|
@@ -132,7 +133,7 @@ When you create a new project, the CLI also scaffolds:
|
|
|
132
133
|
| Command | Description |
|
|
133
134
|
|---|---|
|
|
134
135
|
| `micropage push` | Merge local `.page` files and save as a draft build |
|
|
135
|
-
| `micropage publish` | Push then deploy
|
|
136
|
+
| `micropage publish` | Push then deploy the site live |
|
|
136
137
|
| `micropage builds list [--json]` | List builds for the current project |
|
|
137
138
|
| `micropage builds redeploy [version]` | Re-publish an older build as a new deploy |
|
|
138
139
|
| `micropage builds download [version] [-o file]` | Download a deployed build archive as a `.zip` file |
|
|
@@ -227,7 +228,7 @@ If the project folder contains a root-level `llms.txt`, its contents are publish
|
|
|
227
228
|
Each project folder has a `.micropage/project.json` file that stores:
|
|
228
229
|
- `projectId` — the Supabase project ID
|
|
229
230
|
- `buildId` — the ID of the last draft/deployed build (auto-updated by push/pull)
|
|
230
|
-
- `domain` — the
|
|
231
|
+
- `domain` — the project slug; the site lives at `<domain>.micropage.sh`
|
|
231
232
|
- `name` — the project name
|
|
232
233
|
|
|
233
234
|
Add `.micropage/` to your `.gitignore` if you share the content folder.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@micropage-sh/cli",
|
|
3
|
-
"version": "2.6.
|
|
4
|
-
"description": "CLI
|
|
3
|
+
"version": "2.6.4",
|
|
4
|
+
"description": "Micropage CLI: keep your .page files in git and publish static sites from the terminal",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"micropage": "bin/micropage.js"
|
package/src/commands/posts.js
CHANGED
|
@@ -92,11 +92,84 @@ async function resolveFormId(projectId, listName) {
|
|
|
92
92
|
return matches[0].id;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
const DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
96
|
+
const ISO_DATETIME_RE =
|
|
97
|
+
/^(\d{4}-\d{2}-\d{2})[Tt ](\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)\s*([Zz]|[+-]\d{2}(?::?\d{2})?)?$/;
|
|
98
|
+
// Date-only values are midnight UTC server-side; allow "today" anywhere on
|
|
99
|
+
// Earth (UTC+14) so a local-calendar date isn't rejected as future.
|
|
100
|
+
const DATE_ONLY_FUTURE_SLACK_MS = 14 * 60 * 60 * 1000;
|
|
101
|
+
const DATETIME_FUTURE_SLACK_MS = 5 * 60 * 1000;
|
|
102
|
+
|
|
103
|
+
function utcDateString(d) {
|
|
104
|
+
return d.toISOString().slice(0, 10);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isoOffset(raw) {
|
|
108
|
+
if (!raw || raw === 'z' || raw === 'Z') return 'Z';
|
|
109
|
+
const digits = raw.slice(1).replace(':', '');
|
|
110
|
+
return `${raw[0]}${digits.slice(0, 2)}:${digits.slice(2) || '00'}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isMidnightUtc(d) {
|
|
114
|
+
return (
|
|
115
|
+
d.getUTCHours() === 0 && d.getUTCMinutes() === 0 && d.getUTCSeconds() === 0 && d.getUTCMilliseconds() === 0
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Normalize a front-matter `date` into what upsert-post accepts: `YYYY-MM-DD`
|
|
121
|
+
* or a full ISO timestamp. js-yaml turns unquoted dates into Date objects
|
|
122
|
+
* (UTC), quoted ones stay strings. Returns null when absent; throws on
|
|
123
|
+
* invalid or future values (scheduling isn't supported).
|
|
124
|
+
*/
|
|
125
|
+
function normalizePostDate(value, now = new Date()) {
|
|
126
|
+
if (value === undefined || value === null || value === '') return null;
|
|
127
|
+
|
|
128
|
+
let normalized;
|
|
129
|
+
if (value instanceof Date) {
|
|
130
|
+
if (Number.isNaN(value.getTime())) throw new Error('invalid "date" (not a valid date)');
|
|
131
|
+
normalized = isMidnightUtc(value) ? utcDateString(value) : value.toISOString();
|
|
132
|
+
} else if (typeof value === 'string') {
|
|
133
|
+
const s = value.trim();
|
|
134
|
+
const dateOnly = DATE_ONLY_RE.exec(s);
|
|
135
|
+
if (dateOnly) {
|
|
136
|
+
const d = new Date(`${s}T00:00:00.000Z`);
|
|
137
|
+
if (Number.isNaN(d.getTime()) || utcDateString(d) !== s) {
|
|
138
|
+
throw new Error(`invalid "date" (${s}); use YYYY-MM-DD or an ISO timestamp`);
|
|
139
|
+
}
|
|
140
|
+
normalized = s;
|
|
141
|
+
} else {
|
|
142
|
+
const m = ISO_DATETIME_RE.exec(s);
|
|
143
|
+
// A timestamp without an offset is read as UTC, matching how YAML
|
|
144
|
+
// resolves unquoted timestamps.
|
|
145
|
+
const d = m ? new Date(`${m[1]}T${m[2]}${isoOffset(m[3])}`) : null;
|
|
146
|
+
if (!d || Number.isNaN(d.getTime()) || utcDateString(new Date(`${m[1]}T00:00:00.000Z`)) !== m[1]) {
|
|
147
|
+
throw new Error(`invalid "date" (${s}); use YYYY-MM-DD or an ISO timestamp`);
|
|
148
|
+
}
|
|
149
|
+
normalized = d.toISOString();
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
throw new Error(`invalid "date" (${String(value)}); use YYYY-MM-DD or an ISO timestamp`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const isFuture = DATE_ONLY_RE.test(normalized)
|
|
156
|
+
? Date.parse(`${normalized}T00:00:00Z`) > now.getTime() + DATE_ONLY_FUTURE_SLACK_MS
|
|
157
|
+
: new Date(normalized).getTime() > now.getTime() + DATETIME_FUTURE_SLACK_MS;
|
|
158
|
+
if (isFuture) {
|
|
159
|
+
throw new Error(`"date" ${normalized} is in the future; scheduling posts is not supported`);
|
|
160
|
+
}
|
|
161
|
+
return normalized;
|
|
162
|
+
}
|
|
163
|
+
|
|
95
164
|
function frontMatterFromPost(post) {
|
|
96
165
|
const fmData = {
|
|
97
166
|
title: post.title || '',
|
|
98
167
|
};
|
|
99
168
|
if (post.slug) fmData.slug = post.slug;
|
|
169
|
+
if (post.published_at) {
|
|
170
|
+
const published = new Date(post.published_at);
|
|
171
|
+
if (!Number.isNaN(published.getTime())) fmData.date = utcDateString(published);
|
|
172
|
+
}
|
|
100
173
|
if (post.description) fmData.description = post.description;
|
|
101
174
|
if (post.web_visibility && post.web_visibility !== 'listed') fmData.visibility = post.web_visibility;
|
|
102
175
|
if (post.hero_image) fmData.hero = post.hero_image;
|
|
@@ -202,6 +275,15 @@ async function push(options = {}) {
|
|
|
202
275
|
continue;
|
|
203
276
|
}
|
|
204
277
|
|
|
278
|
+
let postDate;
|
|
279
|
+
try {
|
|
280
|
+
postDate = normalizePostDate(fm.date);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
console.error(`${relName}: ${err.message}`);
|
|
283
|
+
hadError = true;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
205
287
|
const emailWanted = fm.email === true;
|
|
206
288
|
let formId = null;
|
|
207
289
|
if (emailWanted) {
|
|
@@ -270,6 +352,7 @@ async function push(options = {}) {
|
|
|
270
352
|
subject: fm.subject || null,
|
|
271
353
|
preheader: fm.preview || null,
|
|
272
354
|
};
|
|
355
|
+
if (postDate) payload.date = postDate;
|
|
273
356
|
|
|
274
357
|
try {
|
|
275
358
|
const result = await fn.invoke('upsert-post', payload);
|
|
@@ -643,4 +726,5 @@ module.exports = {
|
|
|
643
726
|
slugify,
|
|
644
727
|
defaultSlugFromFilename,
|
|
645
728
|
frontMatterFromPost,
|
|
729
|
+
normalizePostDate,
|
|
646
730
|
};
|
package/src/index.js
CHANGED
|
@@ -19,7 +19,7 @@ const program = new Command();
|
|
|
19
19
|
|
|
20
20
|
program
|
|
21
21
|
.name('micropage')
|
|
22
|
-
.description('CLI
|
|
22
|
+
.description('Micropage CLI: keep your .page files in git and publish static sites from the terminal')
|
|
23
23
|
.version(version);
|
|
24
24
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
@@ -65,7 +65,7 @@ projectCmd
|
|
|
65
65
|
.description('Create a new project and init local folder')
|
|
66
66
|
.option(
|
|
67
67
|
'-d, --domain <domain>',
|
|
68
|
-
'
|
|
68
|
+
'Choose the project slug, which sets its address <domain>.micropage.sh (default: server generates name-slug + 6 hex, max 58 chars)',
|
|
69
69
|
)
|
|
70
70
|
.action((name, opts) => projects.create(name, opts));
|
|
71
71
|
|
|
@@ -87,7 +87,7 @@ projectCmd
|
|
|
87
87
|
|
|
88
88
|
projectCmd
|
|
89
89
|
.command('publish')
|
|
90
|
-
.description('Push local content and publish
|
|
90
|
+
.description('Push local content and publish it live — alias for `micropage publish`')
|
|
91
91
|
.option('-w, --watch', 'Stream build/deploy events after triggering publish')
|
|
92
92
|
.action((opts) => builds.publish(opts));
|
|
93
93
|
|
|
@@ -152,7 +152,7 @@ program
|
|
|
152
152
|
|
|
153
153
|
program
|
|
154
154
|
.command('publish')
|
|
155
|
-
.description('Push local content and publish
|
|
155
|
+
.description('Push local content and publish it live')
|
|
156
156
|
.option('-w, --watch', 'Stream build/deploy events after triggering publish')
|
|
157
157
|
.action((opts) => builds.publish(opts));
|
|
158
158
|
|
|
@@ -23,6 +23,7 @@ Each file in `posts/*.md` is one post: YAML front-matter + a Markdown body, push
|
|
|
23
23
|
---
|
|
24
24
|
title: Launching our new dashboard
|
|
25
25
|
slug: launching-new-dashboard # optional; defaults to the filename minus a leading date prefix and .md
|
|
26
|
+
date: 2026-07-07 # optional; public date, YYYY-MM-DD or ISO timestamp (default: publish time; no future dates)
|
|
26
27
|
description: A quick look at what's new.
|
|
27
28
|
visibility: listed # listed | unlisted | none (default: listed)
|
|
28
29
|
hero: launch-hero.png # optional; local file, existing uploaded asset filename, or absolute URL
|