emdash-emit 0.1.7 → 0.1.9
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 +237 -82
- package/dist/cli/commands/new.d.ts.map +1 -1
- package/dist/cli/commands/new.js +3 -1
- package/dist/cli/commands/new.js.map +1 -1
- package/dist/cli/commands/profile.d.ts.map +1 -1
- package/dist/cli/commands/profile.js +67 -13
- package/dist/cli/commands/profile.js.map +1 -1
- package/dist/cli/commands/pull.d.ts +5 -0
- package/dist/cli/commands/pull.d.ts.map +1 -1
- package/dist/cli/commands/pull.js +96 -39
- package/dist/cli/commands/pull.js.map +1 -1
- package/dist/cli/commands/push.d.ts.map +1 -1
- package/dist/cli/commands/push.js +17 -2
- package/dist/cli/commands/push.js.map +1 -1
- package/dist/cli/commands/status.d.ts +19 -0
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +93 -4
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/context.d.ts +35 -0
- package/dist/cli/context.d.ts.map +1 -1
- package/dist/cli/context.js +112 -10
- package/dist/cli/context.js.map +1 -1
- package/dist/client/auth.d.ts +1 -0
- package/dist/client/auth.d.ts.map +1 -1
- package/dist/client/auth.js +1 -0
- package/dist/client/auth.js.map +1 -1
- package/dist/client/http.d.ts.map +1 -1
- package/dist/client/http.js +27 -10
- package/dist/client/http.js.map +1 -1
- package/dist/client/mapper.d.ts +12 -0
- package/dist/client/mapper.d.ts.map +1 -1
- package/dist/client/mapper.js +65 -0
- package/dist/client/mapper.js.map +1 -1
- package/dist/client/media.d.ts +141 -0
- package/dist/client/media.d.ts.map +1 -0
- package/dist/client/media.js +582 -0
- package/dist/client/media.js.map +1 -0
- package/dist/client/portable-text.d.ts +22 -1
- package/dist/client/portable-text.d.ts.map +1 -1
- package/dist/client/portable-text.js +85 -5
- package/dist/client/portable-text.js.map +1 -1
- package/dist/db/cache.d.ts +16 -0
- package/dist/db/cache.d.ts.map +1 -1
- package/dist/db/cache.js +67 -0
- package/dist/db/cache.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/context.d.ts +1 -0
- package/dist/mcp/context.d.ts.map +1 -1
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +60 -15
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tool-errors.d.ts +11 -0
- package/dist/mcp/tool-errors.d.ts.map +1 -0
- package/dist/mcp/tool-errors.js +62 -0
- package/dist/mcp/tool-errors.js.map +1 -0
- package/dist/mcp/tools/profile.d.ts +9 -2
- package/dist/mcp/tools/profile.d.ts.map +1 -1
- package/dist/mcp/tools/profile.js +49 -36
- package/dist/mcp/tools/profile.js.map +1 -1
- package/dist/mcp/tools/sync-diff.d.ts.map +1 -1
- package/dist/mcp/tools/sync-diff.js +18 -70
- package/dist/mcp/tools/sync-diff.js.map +1 -1
- package/dist/mcp/tools/sync-new.d.ts.map +1 -1
- package/dist/mcp/tools/sync-new.js +40 -21
- package/dist/mcp/tools/sync-new.js.map +1 -1
- package/dist/mcp/tools/sync-pull.d.ts.map +1 -1
- package/dist/mcp/tools/sync-pull.js +110 -86
- package/dist/mcp/tools/sync-pull.js.map +1 -1
- package/dist/mcp/tools/sync-push.d.ts.map +1 -1
- package/dist/mcp/tools/sync-push.js +24 -32
- package/dist/mcp/tools/sync-push.js.map +1 -1
- package/dist/mcp/tools/sync-status.d.ts.map +1 -1
- package/dist/mcp/tools/sync-status.js +55 -4
- package/dist/mcp/tools/sync-status.js.map +1 -1
- package/dist/sync/diff.d.ts +36 -0
- package/dist/sync/diff.d.ts.map +1 -1
- package/dist/sync/diff.js +118 -0
- package/dist/sync/diff.js.map +1 -1
- package/dist/sync/media.d.ts +16 -0
- package/dist/sync/media.d.ts.map +1 -1
- package/dist/sync/media.js +180 -26
- package/dist/sync/media.js.map +1 -1
- package/dist/sync/merge.d.ts.map +1 -1
- package/dist/sync/merge.js +1 -0
- package/dist/sync/merge.js.map +1 -1
- package/dist/sync/orchestrator.d.ts +6 -0
- package/dist/sync/orchestrator.d.ts.map +1 -1
- package/dist/sync/orchestrator.js +52 -27
- package/dist/sync/orchestrator.js.map +1 -1
- package/dist/sync/preflight.d.ts +30 -2
- package/dist/sync/preflight.d.ts.map +1 -1
- package/dist/sync/preflight.js +274 -10
- package/dist/sync/preflight.js.map +1 -1
- package/dist/sync/pull.d.ts +47 -0
- package/dist/sync/pull.d.ts.map +1 -1
- package/dist/sync/pull.js +148 -8
- package/dist/sync/pull.js.map +1 -1
- package/dist/sync/push.d.ts +2 -0
- package/dist/sync/push.d.ts.map +1 -1
- package/dist/sync/push.js +332 -6
- package/dist/sync/push.js.map +1 -1
- package/dist/sync/status.d.ts +11 -1
- package/dist/sync/status.d.ts.map +1 -1
- package/dist/sync/status.js +40 -6
- package/dist/sync/status.js.map +1 -1
- package/dist/sync/taxonomies.d.ts.map +1 -1
- package/dist/sync/taxonomies.js +39 -4
- package/dist/sync/taxonomies.js.map +1 -1
- package/dist/utils/config.d.ts +11 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +14 -4
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +16 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +66 -7
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/profile.d.ts +3 -3
- package/dist/utils/profile.d.ts.map +1 -1
- package/dist/utils/profile.js +105 -8
- package/dist/utils/profile.js.map +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
- [Media Management & Upload Workflow](#media-management--upload-workflow)
|
|
33
33
|
- [🤖 The Golden Path for AI & Humans (4-Step Cycle)](#-the-golden-path-for-ai--humans-4-step-cycle)
|
|
34
34
|
- [The 4-Step Authoring Cycle](#the-4-step-authoring-cycle)
|
|
35
|
-
- [Preflight Safety Guardrails (G1–
|
|
35
|
+
- [Preflight Safety Guardrails (G1–G12)](#preflight-safety-guardrails-g1g12)
|
|
36
36
|
- [🛠 CLI Command Reference](#-cli-command-reference)
|
|
37
37
|
- [`emit init`](#emit-init)
|
|
38
38
|
- [`emit profile`](#emit-profile)
|
|
@@ -78,10 +78,10 @@ To prevent user expectations from misaligning with the tool's purpose, it is imp
|
|
|
78
78
|
|
|
79
79
|
* **What Emit IS (Core Goal)**:
|
|
80
80
|
* An **active authoring and editing workbench (Staging Desk)** where human authors and AI coding agents collaborate seamlessly.
|
|
81
|
-
* A tool focused on drafting, reviewing, polishing, diffing, and publishing current and recent articles with strict safety guards.
|
|
81
|
+
* A tool focused on drafting, reviewing, polishing, diffing, and publishing current and recent articles with strict safety guards—including **automatic offline draft image upload** (`media/drafts/`) for images referenced in the article being pushed.
|
|
82
82
|
* **What Emit is NOT (Non-Goals)**:
|
|
83
|
-
* **Not a full-site database archiver / backup dumper**: Emit is not designed to clone an entire 15-year history of 20,000 blog posts onto your laptop.
|
|
84
|
-
* **Not an offline bulk mass-migration converter**: Emit does not perform batch transformation across thousands of historical posts at once.
|
|
83
|
+
* **Not a full-site database archiver / backup dumper**: Emit is not designed to clone an entire 15-year history of 20,000 blog posts or an entire multi-gigabyte media library onto your laptop.
|
|
84
|
+
* **Not an offline bulk mass-migration converter**: Emit does not perform batch transformation across thousands of historical posts or bulk media migrations at once.
|
|
85
85
|
|
|
86
86
|
If your site contains thousands of articles, they remain safe, searchable, and intact in your Cloudflare D1 database. Emit only pulls the active working set into your local workspace, keeping your directory clean and searches instant. Need to edit a specific older article outside the active window? Simply pull it on demand using `emit pull <article-id>`.
|
|
87
87
|
|
|
@@ -116,7 +116,7 @@ By leveraging Node.js 22's built-in `node:sqlite` module (`DatabaseSync`), Emit
|
|
|
116
116
|
|
|
117
117
|
To prevent concurrent race conditions when multiple authors or AI agents collaborate:
|
|
118
118
|
|
|
119
|
-
* Emit acquires a distributed 7-minute lease via `POST /api/
|
|
119
|
+
* Emit acquires a distributed 7-minute lease via `POST /_emdash/api/content/{collection}/{id}/lock` before writing to remote articles, with automatic heartbeats and `try/finally` release guarantees.
|
|
120
120
|
* If concurrent edits occur, Emit utilizes a **Hybrid 3-Way Merge** engine (`node-diff3`), merging YAML frontmatter fields and Markdown body text independently.
|
|
121
121
|
|
|
122
122
|
---
|
|
@@ -193,6 +193,9 @@ emit profile add my-blog \
|
|
|
193
193
|
> [!NOTE]
|
|
194
194
|
> Profiles are securely stored with `0600` permissions at `~/.config/emit/profiles.yaml` (on macOS / Linux) or `%USERPROFILE%\.config\emit\profiles.yaml` (on Windows).
|
|
195
195
|
|
|
196
|
+
> [!NOTE]
|
|
197
|
+
> **`emit profile add` updates in place.** Re-running it with only `--origin` (for example after rotating a domain) keeps the stored `--token` and the `--workspace` link that `emit init` created, and reports `Kept the existing token and workspace link`. Pass `--token`/`--workspace` explicitly to replace them; an empty `--token ''` clears the credential on purpose.
|
|
198
|
+
|
|
196
199
|
### 2. Initialize a Workspace
|
|
197
200
|
|
|
198
201
|
Navigate to your local repository directory and initialize it:
|
|
@@ -202,7 +205,7 @@ cd /path/to/local/blog-repo
|
|
|
202
205
|
emit init
|
|
203
206
|
```
|
|
204
207
|
|
|
205
|
-
This creates the `.emit/` directory with
|
|
208
|
+
This creates the `.emit/` directory with `config.json` (inheriting `origin` from your resolved profile when `--origin` is omitted, and automatically linking `workspace` back into `~/.config/emit/profiles.yaml` unless `--no-link` is passed) and initializes the local baseline cache SQLite database (`.emit/cache.db`).
|
|
206
209
|
|
|
207
210
|
### 3. Multi-Site Management (Directory-Driven & Zero Friction)
|
|
208
211
|
|
|
@@ -222,11 +225,60 @@ emit profile list
|
|
|
222
225
|
```
|
|
223
226
|
|
|
224
227
|
**How Automatic Profile Resolution Works:**
|
|
225
|
-
1. **Directory-Driven Auto Detection (Recommended)**: When you `cd ~/blogs/personal` and run `emit pull`, `emit push`, or launch an AI agent, Emit detects your current folder and automatically uses the `personal` profile. When you `cd ~/blogs/work`, it seamlessly connects to `work`.
|
|
228
|
+
1. **Directory-Driven Auto Detection (Recommended)**: When you `cd ~/blogs/personal` and run `emit pull`, `emit push`, or launch an AI agent, Emit detects your current folder and automatically uses the `personal` profile. When you `cd ~/blogs/work`, it seamlessly connects to `work`. Directory comparison is symlink-safe (`fs.realpathSync`), so a workspace reached through a symlinked path (or a macOS `/tmp`-style alias) still resolves to the right profile.
|
|
226
229
|
2. **Explicit Profile Flag**: You can explicitly specify `--profile <name>` (or pass `profile: "<name>"` in MCP) from any directory to override automatic detection.
|
|
227
230
|
3. **Ambiguity Safeguard**: If you run a command outside of any registered workspace and omit `--profile`, Emit aborts with an `AmbiguousProfileError` instead of guessing or silently pushing to the wrong site.
|
|
228
231
|
|
|
229
|
-
### 4.
|
|
232
|
+
### 4. Authentication Resolution Order & Token Auto-Refresh
|
|
233
|
+
|
|
234
|
+
Emit resolves authentication credentials using a strict 4-tier priority chain (`src/client/auth.ts`):
|
|
235
|
+
|
|
236
|
+
1. **Environment Variables (`EMDASH_TOKEN` or `EMDASH_API_TOKEN`)**: Highest priority; ideal for CI/CD pipelines and one-off overrides. `EMDASH_ORIGIN` (and `EMDASH_WORKSPACE`) are read here as well and documented under [Origin Resolution Precedence](#origin-resolution-precedence).
|
|
237
|
+
2. **Site Profile Registry (`~/.config/emit/profiles.yaml`)**: Matched via `--profile <name>` (or MCP `profile`), current workspace directory auto-detection, or a single registered profile.
|
|
238
|
+
3. **EmDash CLI Credentials (`~/.config/emdash/auth.json`)**: Fallback when no token is configured in the profile.
|
|
239
|
+
4. **Emit Custom Credentials (`~/.config/emit/credentials.json`)**: Final fallback.
|
|
240
|
+
|
|
241
|
+
**Automatic OAuth Token Refresh on HTTP 401 (`src/client/http.ts`)**:
|
|
242
|
+
When credentials include a `refresh_token` (such as OAuth sessions in `~/.config/emdash/auth.json` or `~/.config/emit/credentials.json`) and a request returns `401 Unauthorized`, Emit automatically exchanges the `refresh_token` at `POST /_emdash/api/auth/token` (`grant_type: "refresh_token"`), persists the refreshed token back to `auth.json` when applicable, and transparently retries the request once.
|
|
243
|
+
|
|
244
|
+
#### Origin Resolution Precedence
|
|
245
|
+
|
|
246
|
+
The target instance (`origin`) is resolved by one shared function (`resolveOriginPriority()` in `src/cli/context.ts`, reused by the MCP server) so CLI and MCP always agree:
|
|
247
|
+
|
|
248
|
+
1. **Explicitly selected profile** — `--profile <name>` (or MCP `profile: "<name>"`)
|
|
249
|
+
2. **`EMDASH_ORIGIN`** — overrides an *implicitly* resolved profile so CI/staging overrides actually take effect
|
|
250
|
+
3. **Auto-resolved profile** — current-directory match, unique origin match, or the single registered profile
|
|
251
|
+
4. **Resolved auth origin** — carried by `~/.config/emdash/auth.json` / `~/.config/emit/credentials.json`
|
|
252
|
+
5. **Workspace `.emit/config.json`** → then the `https://example.com` sentinel default
|
|
253
|
+
|
|
254
|
+
> [!NOTE]
|
|
255
|
+
> Every silent override is surfaced as a `⚠️ Warning` naming the **real source** of each value (`.emit/config.json` values are never mislabeled as `EMDASH_ORIGIN` or the reverse). When you select `--profile` explicitly, `EMDASH_ORIGIN` is ignored **and reported**. Comparisons ignore trailing slashes. `emit init` follows the same ranking (`--origin` > `EMDASH_ORIGIN` > auto-resolved profile) and refuses to link the workspace to a profile whose origin does not match the initialized origin.
|
|
256
|
+
|
|
257
|
+
### 5. Workspace Configuration (`.emit/config.json`)
|
|
258
|
+
|
|
259
|
+
Running `emit init` generates `.emit/config.json` with the following keys (`src/utils/config.ts`):
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"origin": "https://your-blog.example.com",
|
|
264
|
+
"defaultCollection": "posts",
|
|
265
|
+
"defaultLocale": "ja",
|
|
266
|
+
"postsSyncLimit": 100,
|
|
267
|
+
"mediaSyncLimit": 300,
|
|
268
|
+
"concurrency": 8
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
| Key | Default | Description |
|
|
273
|
+
| --- | --- | --- |
|
|
274
|
+
| `origin` | `"https://example.com"` | Base URL of the target EmDash instance, exactly as written to disk. It is never rewritten in-memory by `EMDASH_ORIGIN`; see [Origin Resolution Precedence](#origin-resolution-precedence) for how the effective origin is chosen |
|
|
275
|
+
| `defaultCollection` | `"posts"` | Default content collection when `--collection` is omitted |
|
|
276
|
+
| `defaultLocale` | `"ja"` | Default locale used when scaffolding new drafts via `emit new` / `sync_new` |
|
|
277
|
+
| `postsSyncLimit` | `100` | Sliding window size for batch article pulls (when `status: all`, fetches up to `postsSyncLimit` `published` + up to `postsSyncLimit` `draft` articles in parallel; also supports legacy `syncLimit` fallback) |
|
|
278
|
+
| `mediaSyncLimit` | `300` | Default maximum items fetched by `emit pull-media` / `sync_pull_media` |
|
|
279
|
+
| `concurrency` | `8` | Parallel HTTP fetch concurrency during batch pulls |
|
|
280
|
+
|
|
281
|
+
### 6. Recommended `.gitignore`
|
|
230
282
|
|
|
231
283
|
When tracking your local blog repository with Git, use the following recommended `.gitignore` configuration:
|
|
232
284
|
|
|
@@ -256,9 +308,8 @@ tmp/
|
|
|
256
308
|
> [!TIP]
|
|
257
309
|
> * `.emit/cache.db*` **must be excluded** from Git because it is a machine-local SQLite tracking database.
|
|
258
310
|
> * `.emit/config.json` **can be safely committed** to Git because it contains workspace configuration (origin URL, default collections), while sensitive authentication tokens remain securely isolated in `~/.config/emit/profiles.yaml`.
|
|
259
|
-
> * `media/*.yaml` metadata files can be committed to Git to track asset metadata alongside your Markdown articles.
|
|
260
|
-
>
|
|
261
|
-
>
|
|
311
|
+
> * `media/*.yaml` metadata files can be committed to Git to track asset metadata (`storageKey`, `contentHash`, `status`, dimensions) alongside your Markdown articles.
|
|
312
|
+
> * **Local-First Git Tracking for `media/`**: If you use your Git repository as a self-contained Local-First source of truth (including offline drafts in `media/drafts/`), keep `media/` tracked in Git (or via Git LFS). If you treat Cloudflare R2 as the sole binary store and want a lightweight Git repo, uncomment the `media/*.jpg` ignore rules above while keeping `media/*.yaml` tracked.
|
|
262
313
|
|
|
263
314
|
---
|
|
264
315
|
|
|
@@ -271,68 +322,104 @@ your-blog/
|
|
|
271
322
|
├── .emit/ # [Hidden System Directory - Do not edit manually]
|
|
272
323
|
│ ├── config.json # Workspace configuration (origin, postsSyncLimit, etc.)
|
|
273
324
|
│ └── cache.db # SQLite baseline tracking & hash database
|
|
274
|
-
├── categories/ # Category definition files
|
|
275
|
-
│ ├──
|
|
276
|
-
│ └──
|
|
277
|
-
├── tags/ # Tag definition files
|
|
278
|
-
│ ├──
|
|
279
|
-
│ └──
|
|
280
|
-
├── media/ # Media files and metadata
|
|
281
|
-
│ ├──
|
|
282
|
-
│ └──
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
325
|
+
├── categories/ # Category definition files (<id>-<label>.yaml)
|
|
326
|
+
│ ├── 01M6CAT1-Tech.yaml
|
|
327
|
+
│ └── 01M6CAT2-カメラ.yaml
|
|
328
|
+
├── tags/ # Tag definition files (<id>-<label>.yaml)
|
|
329
|
+
│ ├── 01M6TAG1-Typescript.yaml
|
|
330
|
+
│ └── 01M6TAG2-Ai.yaml
|
|
331
|
+
├── media/ # Media files and companion metadata (<yyyyMMdd>_<id>-<name>.<ext>)
|
|
332
|
+
│ ├── drafts/ # Staging folder for offline draft images (auto-uploaded on push)
|
|
333
|
+
│ │ └── sunset.png
|
|
334
|
+
│ ├── 20261101_01M6ABCDEF-header.jpg
|
|
335
|
+
│ └── 20261101_01M6ABCDEF-header.yaml
|
|
336
|
+
└── posts/ # Local Markdown Articles (<yyyyMMdd>_<ulid>-<title>.md)
|
|
337
|
+
├── draft/ # Draft articles (named after title; falls back to slug if empty)
|
|
338
|
+
│ └── 20261104_01M6...-My-New-Post.md
|
|
339
|
+
├── published/ # Live published articles
|
|
340
|
+
│ └── 20261015_01M5...-Getting-Started.md
|
|
341
|
+
└── conflicts/ # Read-only rescue copies of divergent local content (written by pull; never pushed or pruned)
|
|
342
|
+
└── 20261101_01M5...-Getting-Started.conflict.md
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Local SQLite Cache (`.emit/cache.db`)
|
|
347
|
+
|
|
348
|
+
Emit maintains a local SQLite database via Node.js built-in `node:sqlite` (`src/db/cache.ts`) with 5 tables:
|
|
349
|
+
|
|
350
|
+
| Table | Purpose |
|
|
351
|
+
| --- | --- |
|
|
352
|
+
| `entries` | Caches the latest remote JSON envelope and revision (`rev`) for each synced article |
|
|
353
|
+
| `base_snapshots` | Stores the baseline Markdown body and baseline frontmatter at the last synced revision to power Hybrid 3-Way Merge (`frontmatter_yaml` holds a **JSON-encoded** `Frontmatter` object, not YAML text) |
|
|
354
|
+
| `sync_state` | Tracks each article's workspace-relative file path and SHA-256 content hash to detect local modifications (`synced` vs `modified` vs `untracked`, plus `invalid` when YAML frontmatter fails parsing; `invalid` files are excluded from `emit push --all`, cached `invalid` files are automatically self-healed on batch `emit pull` / `sync_pull`, and uncached `invalid` files outside the sliding window can be repaired via `emit pull <ULID>` or `sync_pull(id: "<ULID>")`) |
|
|
355
|
+
| `bylines` | Caches remote author records (`id`, `slug`, `display_name`, `is_guest`) synced automatically via `/_emdash/api/mcp` (`byline_list`) on pull, enabling slug ⇔ ULID resolution and default author selection |
|
|
356
|
+
| `media_state` | Tracks offline draft image uploads (`local_hash`, `media_id`, `storage_key`, `url`, `workspace_path`, `status`: `pending` \| `uploaded` \| `ready` \| `failed`) for idempotency, retry recovery, and cross-article asset reuse |
|
|
290
357
|
|
|
291
358
|
---
|
|
292
359
|
|
|
293
360
|
## 📝 Markdown & YAML Frontmatter Specification
|
|
294
361
|
|
|
295
|
-
Each article is stored as a Markdown file with a
|
|
362
|
+
Each article is stored as a Markdown file with a validated YAML frontmatter header (`FrontmatterSchema` in `src/utils/frontmatter.ts`).
|
|
296
363
|
|
|
297
364
|
### Frontmatter Schema
|
|
298
365
|
|
|
299
366
|
```yaml
|
|
300
367
|
---
|
|
301
|
-
id: 01M6ABCDEF1234567890123456 #
|
|
368
|
+
id: 01M6ABCDEF1234567890123456 # 26-char canonical ULID (auto-generated; DO NOT EDIT)
|
|
369
|
+
slug: building-ai-powered-cms-tools
|
|
302
370
|
title: "Building AI-Powered CMS Tools"
|
|
303
|
-
|
|
304
|
-
status: draft
|
|
305
|
-
publishedAt: null
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
categories:
|
|
310
|
-
|
|
311
|
-
tags:
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
371
|
+
collection: posts # Target collection (defaults to "posts")
|
|
372
|
+
status: draft # "draft" or "published" (defaults to "draft")
|
|
373
|
+
publishedAt: null # ISO 8601 timestamp string (e.g. "2026-11-04T12:00:00.000Z") or null
|
|
374
|
+
locale: ja # Locale code (defaults to "ja")
|
|
375
|
+
featuredImage: "01M67P0MBMJQQXH2K95HHCYRYB" # Optional: 26-char media ULID (uppercase-normalized) or null
|
|
376
|
+
taxonomies: # Map of taxonomy key -> array of term slugs (or canonical term ULIDs)
|
|
377
|
+
categories:
|
|
378
|
+
- tech
|
|
379
|
+
tags:
|
|
380
|
+
- typescript
|
|
381
|
+
- ai
|
|
382
|
+
seo: # Optional SEO metadata block
|
|
383
|
+
title: null # Custom SEO title string or null
|
|
384
|
+
description: null # Meta description string or null
|
|
385
|
+
canonical: null # Absolute URL (e.g. "https://example.com/posts/...") or null
|
|
386
|
+
noIndex: false # Boolean (optional)
|
|
387
|
+
image: null # SEO image string or null
|
|
388
|
+
bylines: # Author byline slugs (resolved via cached bylines table)
|
|
389
|
+
- aroooy
|
|
316
390
|
---
|
|
317
391
|
|
|
318
392
|
Markdown body begins here...
|
|
319
393
|
|
|
320
394
|
```
|
|
321
395
|
|
|
396
|
+
> [!IMPORTANT]
|
|
397
|
+
> **Schema Validation & Serialization Rules (`src/utils/frontmatter.ts`)**:
|
|
398
|
+
> * **Always nest categories and tags under `taxonomies:`**: `parseMarkdown()` validates frontmatter with Zod (`FrontmatterSchema`), which strips unknown top-level keys, and `serializeMarkdown()` regenerates the YAML header on every pull and push. Writing `categories:` or `tags:` at the top level will silently discard them without triggering Preflight Guard G4. `taxonomies` accepts `categories`, `tags`, and any additional custom taxonomy keys defined in EmDash (`z.record(z.array(z.string()))`).
|
|
399
|
+
> * **`seo.canonical` must be an absolute URL**: Validated via `z.string().url().nullable().optional()`. Relative paths such as `"/posts/my-slug"` will fail frontmatter parsing; always specify a full `https://...` URL or `null`.
|
|
400
|
+
> * **Author `bylines` auto-selection**: When scaffolding with `emit new` / `sync_new` without `--byline` / `byline`, Emit populates `bylines` with the first cached non-guest author ordered by `slug ASC` from `.emit/cache.db`. On single-author sites this is always your author slug; on multi-author sites, pass `-b, --byline <slug>` (or `byline` in MCP) or edit `bylines:` in frontmatter explicitly.
|
|
401
|
+
> * **`id` and `featuredImage` normalization**: `id` must be an exact 26-character ULID. `featuredImage` values matching a 26-character Crockford base32 ULID are normalized to uppercase automatically.
|
|
402
|
+
|
|
322
403
|
### Critical Content Rules
|
|
323
404
|
|
|
324
405
|
1. **Do Not Duplicate the Title in the Body**:
|
|
325
406
|
- The article title is managed by the `title:` field in the frontmatter.
|
|
326
407
|
- **Do not** add `# Article Title` as the first heading in the Markdown body. Doing so causes duplicate title displays on the site frontend. (Emit's preflight guard G7 will detect and automatically trim redundant top headings).
|
|
327
408
|
2. **Taxonomy Slugs & Normalization**:
|
|
328
|
-
- Items under `categories
|
|
409
|
+
- Items under `taxonomies.categories` and `taxonomies.tags` should use existing slug names from `categories/*.yaml` and `tags/*.yaml` (e.g. `- tech`, `- nodejs`). Note that while taxonomy definition filenames include the ULID (`categories/<id>-<slug>.yaml`), the frontmatter entries use the **slug**.
|
|
329
410
|
- If a valid term ULID is specified in the frontmatter, Emit automatically resolves it transparently to its corresponding slug for Preflight Guard G4 validation and maintains accurate term assignment on remote sync.
|
|
330
411
|
- Do not duplicate the same term using both its slug and ULID within the same taxonomy (e.g. `[- tech, 01M6CAT1]`). Emit de-duplicates identical term assignments, but a slug and a ULID that resolve to **different** terms will both be assigned to the article.
|
|
331
|
-
- Never invent arbitrary IDs or invalid names; valid options are automatically displayed during `emit pull`, `emit pull-taxonomies`, `emit new`, `sync_pull`, `sync_pull_taxonomies`, and `sync_new`. Preflight Guard G4 strictly validates taxonomy terms before pushing.
|
|
412
|
+
- Never invent arbitrary IDs or invalid names; valid options are automatically displayed as hints during `emit pull`, `emit pull-taxonomies`, `emit new`, `sync_pull`, `sync_pull_taxonomies`, and `sync_new`. Hint lists are capped per command (with an `...and N more` marker), so the authoritative full list is always the `categories/` and `tags/` YAML files. Preflight Guard G4 strictly validates taxonomy terms before pushing.
|
|
332
413
|
3. **Status Transitions**:
|
|
333
414
|
- To publish a draft, update `status: published` in the frontmatter and execute `emit push <filePath> --allow-publish`.
|
|
334
415
|
- Emit automatically updates the remote state and moves the file from `posts/draft/` to `posts/published/`.
|
|
335
416
|
- **Never move or rename files manually using `mv`**.
|
|
417
|
+
4. **Featured Image References**:
|
|
418
|
+
- `featuredImage` must be a 26-character canonical media ULID (e.g. `01M67P0MBMJQQXH2K95HHCYRYB`) or `null`. Emit normalizes the value to uppercase, so lowercase input is accepted and canonicalized on pull, merge, and push.
|
|
419
|
+
- To remove the hero image, set `featuredImage: null`. Omitting the field leaves the remote value untouched.
|
|
420
|
+
- When the referenced ULID matches the cached remote entry, Emit resends the original remote `featured_image` object verbatim (provider, `src`, `meta.storageKey`, dimensions, MIME type) so nothing is lost. When it does not match, Emit sends the bare `{ id: <ULID> }` and lets EmDash resolve the media record. **This contract is confirmed against a live EmDash instance:** `featured_image: { "id": "<ULID>" }` is accepted (HTTP 200) and the server stores the id and normalizes `"provider": "local"` itself.
|
|
421
|
+
- Emit never fabricates media metadata from local `media/*.yaml` files: companion files are used only to cross-check a reference. In a workspace that mirrors media (`media/` exists and is non-empty), a missing or mismatched companion file is reported as a push warning — it means Emit could not confirm the media ID exists, not that the payload is invalid. Workspaces that do not mirror media produce no such warning.
|
|
422
|
+
- Preflight Guard **G10** rejects local file paths and malformed values before pushing.
|
|
336
423
|
|
|
337
424
|
### EmDash Custom Block Preservation
|
|
338
425
|
|
|
@@ -348,18 +435,46 @@ EmDash supports rich visual blocks (e.g., galleries, callouts, audio players). W
|
|
|
348
435
|
|
|
349
436
|
### Media Management & Upload Workflow
|
|
350
437
|
|
|
351
|
-
EmDash stores media files (images, documents) in Cloudflare R2
|
|
438
|
+
EmDash stores media files (images, documents) in Cloudflare R2 and uses **two distinct identifier spaces**:
|
|
439
|
+
|
|
440
|
+
| Usage | Identifier Used | Format / Example |
|
|
441
|
+
| --- | --- | --- |
|
|
442
|
+
| Frontmatter `featuredImage` | **Media Record `id`** | 26-character ULID (`01M67P0MBMJQQXH2K95HHCYRYB`) or `null` |
|
|
443
|
+
| Body `` URL | **Media `storageKey`** | `/_emdash/api/media/file/<storageKey>` (e.g. `/_emdash/api/media/file/01M67P07X2VPW0MYRRRGMA6T5A.jpg` or double-ULID `01M6TF1XXBSC06QQG9M648B4K3.01M6TF1Y5YVG9YYQAY6H25270M.png`) |
|
|
444
|
+
|
|
445
|
+
> [!IMPORTANT]
|
|
446
|
+
> Never put a media record `id` into `/_emdash/api/media/file/<id>.<ext>`—the public file route resolves by **`storageKey`**, so using a record `id` returns HTTP 404.
|
|
447
|
+
|
|
448
|
+
#### Workflow A: Offline Draft Image Auto-Upload (`media/drafts/` — Recommended for Body Images)
|
|
449
|
+
|
|
450
|
+
You can author articles completely offline using local images and let `emit push` handle uploading, URL rewriting, and local file promotion automatically:
|
|
352
451
|
|
|
353
|
-
**
|
|
452
|
+
1. **Stage Image in `media/drafts/`**: Place your image in `<workspace>/media/drafts/<filename>` (supported extensions: `.jpg`, `.jpeg`, `.png`, `.webp`, `.gif`, `.avif`, `.svg`; max 50 MiB).
|
|
453
|
+
2. **Reference on a Standalone Line**: In your Markdown body, reference the staged image on its own standalone line:
|
|
454
|
+
```markdown
|
|
455
|
+

|
|
456
|
+
```
|
|
457
|
+
3. **Push the Article (`emit push` / `sync_push`)**: Emit automatically:
|
|
458
|
+
- Uploads the image via EmDash's 4-stage flow (`POST /_emdash/api/media/upload-url` with `sha1:<hex>` deduplication → `PUT` upload → `POST /_emdash/api/media/{id}/confirm`), cleaning up any orphan `pending` record if an upload fails midway.
|
|
459
|
+
- Rewrites the Markdown line in place to the canonical root-relative `/_emdash/api/media/file/<storageKey>` URL.
|
|
460
|
+
- Pushes the article to EmDash.
|
|
461
|
+
- Promotes the local file from `media/drafts/<filename>` to `media/<yyyyMMdd>_<mediaId>-<filename>` alongside a companion `.yaml` metadata file (`storageKey`, `contentHash`, `status`). The `<yyyyMMdd>` prefix reflects the media record's remote `createdAt` when available; if Emit only had a provisional local date, the next `emit pull-media` canonicalizes the filename.
|
|
462
|
+
4. **Reuse Across Articles (no re-upload):** The same `media/drafts/<filename>` line can be used in several articles. After the first push promotes the asset, every later push resolves that reference through `media_state` / `media/*.yaml` and rewrites it to the already-confirmed `/_emdash/api/media/file/<storageKey>` URL instead of uploading again (reported as `[LOCAL_IMAGE_AUTO_UPLOAD] Reused N already-promoted media asset(s)`). A direct reference to the promoted path works as well. Emit verifies each reused record with `GET /_emdash/api/media/{id}`: if the record was deleted on the EmDash side the push aborts with `PROMOTED_MEDIA_NOT_FOUND_REMOTE` (run `emit pull-media` or re-stage the image in `media/drafts/`), and servers without that endpoint push successfully but warn with `[MEDIA_EXISTENCE_UNVERIFIED]`.
|
|
354
463
|
|
|
355
|
-
|
|
356
|
-
|
|
464
|
+
#### Workflow B: Pre-Uploaded Media & Featured Hero Images
|
|
465
|
+
|
|
466
|
+
1. **Upload via Admin / EmDash MCP**: Upload images via the **EmDash Admin Web Dashboard** (`/admin/media`) or via the official EmDash MCP tool (`media_upload`).
|
|
467
|
+
2. **Sync Media Locally (optional)**: Run `emit pull-media` in your workspace to download images and companion `.yaml` metadata files into `media/`. Note that `emit pull-media --prune` safely ignores `media/drafts/`.
|
|
357
468
|
3. **Embed in Articles**:
|
|
358
|
-
- For featured hero images: set `featuredImage: "
|
|
359
|
-
- For
|
|
469
|
+
- For featured hero images: set `featuredImage: "01M67P0MBMJQQXH2K95HHCYRYB"` (26-character canonical media record `id`) or `null` in frontmatter.
|
|
470
|
+
- For body images: use ``.
|
|
360
471
|
|
|
361
472
|
> [!WARNING]
|
|
362
|
-
> **
|
|
473
|
+
> **Local Image Guardrails (G5 & G11)**:
|
|
474
|
+
> - Arbitrary local paths (e.g. ``, absolute filesystem paths, or a `media/` path that does not resolve to a promoted media record) are blocked by Preflight Guard **G5 (`LOCAL_IMAGE_DETECTED`)**. Paths under `media/drafts/` and already-promoted `media/<yyyyMMdd>_<id>-<name>.<ext>` paths are the only accepted local forms.
|
|
475
|
+
> - Local images placed inline inside a paragraph (e.g. `Text  more text`) are blocked with `INLINE_LOCAL_IMAGE_NOT_SUPPORTED` because EmDash Portable Text requires images to be standalone top-level blocks.
|
|
476
|
+
> - A `media/drafts/` reference whose file no longer exists and which has no promoted match is blocked with `MISSING_LOCAL_IMAGE_FILE` (the promoted location is reported in the error).
|
|
477
|
+
> - Only **backtick** fences (```` ``` ````) are exempt from image scanning. Tilde fences (`~~~`) are not recognized as code by the Portable Text mapper, so they are scanned like normal prose (and emit a `G8` warning because they degrade to plain paragraphs). Write code examples with backticks.
|
|
363
478
|
|
|
364
479
|
---
|
|
365
480
|
|
|
@@ -390,21 +505,24 @@ Whether you are a human developer or an autonomous AI agent (Antigravity, Claude
|
|
|
390
505
|
* To push a draft: `emit push posts/draft/<filename>.md`
|
|
391
506
|
* To publish live: update `status: published` and run `emit push posts/draft/<filename>.md --allow-publish`.
|
|
392
507
|
|
|
393
|
-
### Preflight Safety Guardrails (G1–
|
|
508
|
+
### Preflight Safety Guardrails (G1–G12)
|
|
394
509
|
|
|
395
|
-
To ensure autonomous AI agents and automated scripts cannot corrupt workspace state or accidentally publish unintended content, Emit enforces
|
|
510
|
+
To ensure autonomous AI agents and automated scripts cannot corrupt workspace state or accidentally publish unintended content, Emit enforces 12 strict preflight guardrails before executing any write operation:
|
|
396
511
|
|
|
397
512
|
| Guard | Code Name | Description | Severity |
|
|
398
513
|
| --- | --- | --- | --- |
|
|
399
514
|
| **G1** | `SINGLE_TARGET_GUARD` | Enforces explicit target (`filePath`, `id`, or `--all`) for push/pull; forbids blind bulk push without inspection | Error |
|
|
400
515
|
| **G2** | `ID_MISMATCH_ERROR` | Verifies that frontmatter `id` exactly matches the filename ULID; forbids manual `mv` renames | Error |
|
|
401
516
|
| **G3** | `UNINTENDED_PUBLISH_GUARD` | Rejects promoting `draft` to `published` unless `--allow-publish` (`allowPublish: true`) is explicitly given | Error |
|
|
402
|
-
| **G4** | `INVALID_TAXONOMY_ERROR` | Validates
|
|
403
|
-
| **G5** | `LOCAL_IMAGE_DETECTED` |
|
|
517
|
+
| **G4** | `INVALID_TAXONOMY_ERROR` | Validates taxonomy terms against local definitions (transparently resolves valid term ULIDs) to stop AI taxonomy hallucination | Error |
|
|
518
|
+
| **G5** | `LOCAL_IMAGE_DETECTED` / `INLINE_LOCAL_IMAGE_NOT_SUPPORTED` / `MISSING_LOCAL_IMAGE_FILE` | Rejects local image paths other than `media/drafts/<file>` (or an already-promoted `media/<yyyyMMdd>_<id>-<name>.<ext>` asset) and rejects inline paragraph local images (must be a standalone line) | Error |
|
|
404
519
|
| **G6** | `UNRESOLVED_CONFLICT_MARKERS` | Scans for Git 3-way merge conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) and blocks push | Error |
|
|
405
520
|
| **G7** | `TITLE_DUPLICATION_AUTOTRIM` | Detects redundant `# Title` headings at the top of the body and automatically trims them safely | Warning |
|
|
406
521
|
| **G8** | `UNSUPPORTED_SYNTAX_WARNING` | Warns if unsupported block HTML or incompatible constructs are detected in Markdown | Warning |
|
|
407
522
|
| **G9** | `MODIFIED_OPAQUE_BLOCK` | Verifies that `<!--ec:block ... -->` opaque fences have not been tampered with or corrupted | Error |
|
|
523
|
+
| **G10** | `INVALID_FEATURED_IMAGE` | Verifies that `featuredImage` is a 26-character canonical media ULID (normalized to uppercase) or `null`. A remote media URL that contains a ULID is resolved with a warning; local file paths and malformed values are rejected | Error |
|
|
524
|
+
| **G11** | `LOCAL_IMAGE_AUTO_UPLOAD` (`MISSING_LOCAL_IMAGE_FILE` / `INVALID_LOCAL_IMAGE_TYPE` / `LOCAL_IMAGE_SIZE_EXCEEDED` / `PROMOTED_MEDIA_NOT_FOUND_REMOTE` / `[MEDIA_EXISTENCE_UNVERIFIED]`) | Detects standalone `media/drafts/<file>` image lines (or reuses an already-promoted `media/` asset across articles), validates file existence, extension, and size (≤ 50 MiB), queues them for automatic upload, URL rewrite, and `media/` promotion during `push`, and verifies every reused asset against `GET /_emdash/api/media/{id}` before its URL is baked into the body | Info / Warning / Error |
|
|
525
|
+
| **G12** | `QUARANTINED_COPY_NOT_PUSHABLE` | Refuses to push a rescue copy under `posts/conflicts/` (written by a pull that found divergent local content) before any network request, because it would overwrite the canonical article of the same ULID with stale content and recreate duplicate local files | Error |
|
|
408
526
|
|
|
409
527
|
---
|
|
410
528
|
|
|
@@ -460,7 +578,7 @@ emit profile use <name>
|
|
|
460
578
|
Scaffolds a new draft article with a canonical 26-character ULID timestamp.
|
|
461
579
|
|
|
462
580
|
```bash
|
|
463
|
-
emit new <slug> [-t, --title <title>] [-c, --collection <collection>] [-l, --locale <locale>]
|
|
581
|
+
emit new <slug> [-t, --title <title>] [-c, --collection <collection>] [-l, --locale <locale>] [-b, --byline <slug>] [-p, --profile <name>]
|
|
464
582
|
|
|
465
583
|
```
|
|
466
584
|
|
|
@@ -468,6 +586,8 @@ emit new <slug> [-t, --title <title>] [-c, --collection <collection>] [-l, --loc
|
|
|
468
586
|
* `-t, --title <title>`: Human-readable article title.
|
|
469
587
|
* `-c, --collection <name>`: Collection name (defaults to `posts`).
|
|
470
588
|
* `-l, --locale <locale>`: Locale code (defaults to `ja` or config).
|
|
589
|
+
* `-b, --byline <slug>`: Author byline slug (defaults to the first cached non-guest author ordered by `slug ASC`).
|
|
590
|
+
* `-p, --profile <name>`: Explicitly target a specific connection profile.
|
|
471
591
|
|
|
472
592
|
**Example:**
|
|
473
593
|
|
|
@@ -480,7 +600,7 @@ emit new ai-workflow-guide --title "AI Workflow Guide"
|
|
|
480
600
|
|
|
481
601
|
```text
|
|
482
602
|
✅ Created new article scaffold (ID: 01M6ABCDEF1234567890123456)
|
|
483
|
-
Path: posts/draft/20261104_01M6ABCDEF1234567890123456-
|
|
603
|
+
Path: posts/draft/20261104_01M6ABCDEF1234567890123456-AI-Workflow-Guide.md
|
|
484
604
|
|
|
485
605
|
Available Categories:
|
|
486
606
|
- tech (技術)
|
|
@@ -490,7 +610,7 @@ Available Tags:
|
|
|
490
610
|
- ai, cloudflare, emit, typescript
|
|
491
611
|
|
|
492
612
|
Next: Write your content in Markdown, then run:
|
|
493
|
-
emit push posts/draft/20261104_01M6ABCDEF1234567890123456-
|
|
613
|
+
emit push posts/draft/20261104_01M6ABCDEF1234567890123456-AI-Workflow-Guide.md
|
|
494
614
|
|
|
495
615
|
```
|
|
496
616
|
|
|
@@ -506,21 +626,24 @@ emit pull [id] [options]
|
|
|
506
626
|
```
|
|
507
627
|
|
|
508
628
|
* `[id]`: Optional article ID or ULID. If specified, pulls only that specific article (even if outside the window). Taxonomies are not pulled in single-article mode.
|
|
509
|
-
* `-s, --status <draft|published|all>`: Filter by publication status (default: `all`).
|
|
629
|
+
* `-s, --status <draft|published|all>`: Filter by publication status (default: `all`). When set to `all`, the sliding window (`postsSyncLimit`, default `100`) is applied **per status** (fetching up to `postsSyncLimit` `published` + up to `postsSyncLimit` `draft` articles in parallel).
|
|
510
630
|
* `-c, --collection <collection>`: Collection name (default: `posts`).
|
|
511
631
|
* `--no-prune`: Do not prune local articles that have fallen outside the sliding window (escape hatch; not intended for routine use).
|
|
512
632
|
* `--force`: Force overwrite local files and bypass differential hash checking.
|
|
513
633
|
* `--concurrency <n>`: Parallel processing task limit (default: `8`).
|
|
514
|
-
* `--posts-only`: Skip automatic taxonomy
|
|
515
|
-
* `--media`: Also pull media assets and companion metadata into `media
|
|
634
|
+
* `--posts-only`: Skip automatic remote taxonomy sync. Pulls articles only (local taxonomy candidates hint is still displayed from cached definitions).
|
|
635
|
+
* `--media`: Also pull media assets and companion metadata into `media/` (bounded by `mediaSyncLimit`, default `300`). Use `emit pull-media` for `--limit`, `--all`, and `--prune` control.
|
|
516
636
|
* `--metadata-only`: With `--media`: download only metadata YAML, skip binary file downloads.
|
|
517
637
|
|
|
518
638
|
> [!NOTE]
|
|
519
|
-
> **Taxonomies are pulled by default in batch mode.** Categories and tags are lightweight (2 API calls) and required for valid push operations (Preflight Guard G4 validates taxonomy
|
|
639
|
+
> **Taxonomies are pulled by default in batch mode.** Categories and tags are lightweight (2 API calls) and required for valid push operations (Preflight Guard G4 validates taxonomy terms against these local definitions). This eliminates a common source of push failures. Use `--posts-only` if you need articles only.
|
|
520
640
|
|
|
521
641
|
> [!NOTE]
|
|
522
642
|
> **Why `--no-prune` is rarely needed:**
|
|
523
|
-
> Automatic pruning is mathematically loss-free: Emit's unpushed edits safeguard strictly
|
|
643
|
+
> Automatic pruning is mathematically loss-free: Emit's unpushed edits safeguard inspects both the canonical file and any duplicate filenames sharing the same 26-character ULID, strictly aborting the pull (`SyncPruneConflictError`) if any of them contain local modifications. Clean duplicate files are removed alongside the canonical file. You only need `--no-prune` in exceptional circumstances, such as temporarily suppressing Git working-tree changes during a quick pull.
|
|
644
|
+
|
|
645
|
+
> [!NOTE]
|
|
646
|
+
> **Unpushed writing is never discarded.** When a pull finds local content that diverges from the last synced baseline, Emit first moves that file to `posts/conflicts/<name>.conflict.md` (a read-only rescue copy) and then writes the canonical article. Rescue copies are never pushed or pruned automatically, and pushing one directly is refused with `QUARANTINED_COPY_NOT_PUSHABLE` (Guard **G12**): read the copy, merge any valuable content into the canonical article, then delete it. If an invalid file also carries local edits, `--force` is the escape hatch (it quarantines the divergent copy, then writes the repaired file).
|
|
524
647
|
|
|
525
648
|
**Example:**
|
|
526
649
|
|
|
@@ -547,11 +670,19 @@ emit pull 01M5XYZ9876543210123456789
|
|
|
547
670
|
```text
|
|
548
671
|
Syncing categories and tags...
|
|
549
672
|
✅ Taxonomies: 12 categories, 34 tags
|
|
550
|
-
Fetching articles from remote (posts, status: all, window: 100, concurrency: 8)...
|
|
551
|
-
[1/100] ✅ Pulled hello-world -> posts/published/20261101_01M5...-Hello-World.md
|
|
552
|
-
[2/100] ✅ Pulled draft-post -> posts/draft/20261104_01M6...-Draft-Post.md
|
|
553
673
|
|
|
554
|
-
|
|
674
|
+
Available Categories:
|
|
675
|
+
- tech (技術)
|
|
676
|
+
- column (コラム)
|
|
677
|
+
|
|
678
|
+
Available Tags:
|
|
679
|
+
- ai, cloudflare, emit, typescript
|
|
680
|
+
|
|
681
|
+
Fetching articles from remote (posts, status: all, window: 100/status, concurrency: 8)...
|
|
682
|
+
[1/169] ✅ Pulled hello-world -> posts/published/20261101_01M5...-Hello-World.md
|
|
683
|
+
[2/169] ✅ Pulled draft-post -> posts/draft/20261104_01M6...-Draft-Post.md
|
|
684
|
+
|
|
685
|
+
🎉 Articles: 169 total [100 published, 69 draft] (12 updated, 157 skipped/up-to-date)
|
|
555
686
|
|
|
556
687
|
```
|
|
557
688
|
|
|
@@ -597,16 +728,20 @@ emit status [-c, --collection <collection>] [-p, --profile <name>]
|
|
|
597
728
|
|
|
598
729
|
```text
|
|
599
730
|
--- Workspace Status (posts | origin: https://your-blog.example.com) ---
|
|
600
|
-
🟡 [modified] draft posts/draft/20261104_01M6BBB...-
|
|
601
|
-
⚪ [untracked] draft posts/draft/20261104_01M6AAA...-
|
|
602
|
-
🟢 [synced ] published posts/published/20261015_01M5ZZZ...-
|
|
731
|
+
🟡 [modified] draft posts/draft/20261104_01M6BBB...-AI-Workflow-Guide.md (AI Workflow Guide)
|
|
732
|
+
⚪ [untracked] draft posts/draft/20261104_01M6AAA...-Draft-Notes.md (Draft Notes)
|
|
733
|
+
🟢 [synced ] published posts/published/20261015_01M5ZZZ...-Getting-Started.md (Getting Started)
|
|
734
|
+
🔴 [invalid ] published posts/published/20190405_01M6CCC...-Broken-Post.md (Broken-Post) [uncached] — bad indentation of a mapping entry at line 5
|
|
603
735
|
|
|
604
|
-
Summary:
|
|
736
|
+
Summary: 4 total (1 synced, 1 modified, 1 untracked, 1 invalid)
|
|
605
737
|
```
|
|
606
738
|
|
|
607
739
|
> [!NOTE]
|
|
608
740
|
> Articles are sorted newest-first by date prefix and ULID. The date prefix reflects the article's publication or creation date, not the last-modified time. To locate currently active edits, look for files marked `[modified]`.
|
|
609
741
|
|
|
742
|
+
> [!NOTE]
|
|
743
|
+
> `[invalid]` marks a file whose YAML frontmatter could not be parsed (usually an older serializer wrote unquoted percent-encoded taxonomy slugs). Batch `emit pull` self-heals cached invalid files; files outside the sliding window are marked `[uncached]` and are repaired with `emit pull <ULID>` (MCP: `sync_pull` with `id: "<ULID>"`). When `<collection>/conflicts/` holds rescue copies, the count is reported separately (`conflicts`) and never counted as an article.
|
|
744
|
+
|
|
610
745
|
---
|
|
611
746
|
|
|
612
747
|
### `emit diff`
|
|
@@ -614,15 +749,19 @@ Summary: 3 total (1 synced, 1 modified, 1 untracked)
|
|
|
614
749
|
Shows line-by-line differences between the working Markdown file and the baseline snapshot.
|
|
615
750
|
|
|
616
751
|
```bash
|
|
617
|
-
emit diff [filePath]
|
|
752
|
+
emit diff [filePath] [-i, --id <id>] [-p, --profile <name>]
|
|
618
753
|
|
|
619
754
|
```
|
|
620
755
|
|
|
756
|
+
* `[filePath]`: Relative path to the local Markdown file (e.g., `posts/draft/20261104_01M6...-my-article.md`).
|
|
757
|
+
* `-i, --id <id>`: Inspect by 26-character article ULID instead of file path.
|
|
758
|
+
* `-p, --profile <name>`: Explicitly target a specific connection profile.
|
|
759
|
+
|
|
621
760
|
---
|
|
622
761
|
|
|
623
762
|
### `emit pull-taxonomies`
|
|
624
763
|
|
|
625
|
-
Downloads categories and tags from EmDash and generates local YAML definitions under `categories/` and `tags/`. This is also performed automatically by `emit pull` in batch mode.
|
|
764
|
+
Downloads categories and tags from EmDash and generates local YAML definitions under `categories/` and `tags/`. This is also performed automatically by `emit pull` in batch mode. If one taxonomy endpoint fails, existing local definitions for that taxonomy are preserved with a warning; if both endpoints fail, the command exits with code `1`.
|
|
626
765
|
|
|
627
766
|
```bash
|
|
628
767
|
emit pull-taxonomies
|
|
@@ -635,6 +774,13 @@ emit pull-taxonomies
|
|
|
635
774
|
Pulling categories and tags from remote...
|
|
636
775
|
✅ Successfully synced taxonomies: 12 categories, 34 tags.
|
|
637
776
|
|
|
777
|
+
Available Categories:
|
|
778
|
+
- tech (技術)
|
|
779
|
+
- column (コラム)
|
|
780
|
+
|
|
781
|
+
Available Tags:
|
|
782
|
+
- ai, cloudflare, emit, typescript
|
|
783
|
+
|
|
638
784
|
```
|
|
639
785
|
|
|
640
786
|
---
|
|
@@ -731,15 +877,18 @@ Emit features a first-class **Model Context Protocol (MCP)** server over stdio.
|
|
|
731
877
|
|
|
732
878
|
| Tool | Purpose | Key Parameters |
|
|
733
879
|
| --- | --- | --- |
|
|
734
|
-
| `sync_status` | Inspect local sync state (articles
|
|
735
|
-
| `sync_pull` | Pull remote articles **and taxonomies** into local workspace | `id`, `
|
|
736
|
-
| `sync_new` | Scaffold new article with canonical ULID and
|
|
737
|
-
| `sync_push` | Push local article changes to remote with preflight checks | `filePath
|
|
738
|
-
| `sync_diff` | Inspect diff between working file and baseline snapshot | `filePath` |
|
|
739
|
-
| `sync_pull_taxonomies` | Pull remote category and tag definitions (standalone;
|
|
740
|
-
| `sync_pull_media` | Pull remote media files and metadata into `media/` | `limit`, `all`, `
|
|
880
|
+
| `sync_status` | Inspect local sync state (articles newest-first, plus `invalid` and `conflicts` diagnostics) | `collection`, `profile` |
|
|
881
|
+
| `sync_pull` | Pull remote articles **and taxonomies** into local workspace (includes taxonomy hints; preserves divergent local copies under `posts/conflicts/`) | `collection`, `id`, `filePath`, `status`, `force`, `noPrune`, `prune`, `postsOnly`, `includeMedia`, `metadataOnly`, `profile` |
|
|
882
|
+
| `sync_new` | Scaffold new article with canonical ULID, frontmatter, default byline, and taxonomy hints | `slug` (required), `title`, `collection`, `locale`, `byline`, `profile` |
|
|
883
|
+
| `sync_push` | Push local article changes to remote with preflight checks and `media/drafts/*` auto-upload; refuses `posts/conflicts/` rescue copies (G12) | `filePath` (required), `allowPublish`, `profile` |
|
|
884
|
+
| `sync_diff` | Inspect diff between working file and baseline snapshot | `filePath`, `id`, `profile` |
|
|
885
|
+
| `sync_pull_taxonomies` | Pull remote category and tag definitions (standalone; returns taxonomy hints) | `profile` |
|
|
886
|
+
| `sync_pull_media` | Pull remote media files and metadata into `media/` | `limit`, `all`, `downloadFiles`, `profile` |
|
|
741
887
|
| `profile_list` | List all configured connection profiles | (none) |
|
|
742
|
-
| `profile_switch` | Switch active profile for multi-site environments | `name` |
|
|
888
|
+
| `profile_switch` | Switch active profile for multi-site environments | `name` (required), `persist` |
|
|
889
|
+
|
|
890
|
+
> [!NOTE]
|
|
891
|
+
> **Every tool failure is a structured result, never a raw throw.** Handlers return `isError: true` with the same diagnostics the CLI prints — `[CODE]`, HTTP status, endpoint, and a `💡 Suggestion` line (or a `PREFLIGHT_VALIDATION_FAILED` JSON payload with guard codes). This includes workspace/profile resolution failures, which list the available profiles so the agent can retry with an explicit `profile` argument.
|
|
743
892
|
|
|
744
893
|
### Autonomous Agent Workflows
|
|
745
894
|
|
|
@@ -757,7 +906,7 @@ With Emit connected via MCP, you can give high-level natural language instructio
|
|
|
757
906
|
|
|
758
907
|
To eliminate race conditions when multiple users or agents edit the same post:
|
|
759
908
|
|
|
760
|
-
1. Emit issues a `POST /api/
|
|
909
|
+
1. Emit issues a `POST /_emdash/api/content/{collection}/{id}/lock` request to EmDash before pushing.
|
|
761
910
|
2. EmDash grants a 7-minute mutual exclusion lease.
|
|
762
911
|
3. If another author currently holds the lock, Emit aborts with an informative message indicating who owns the lock and when the lease expires.
|
|
763
912
|
4. Emit guarantees lock release using strict `try/finally` execution.
|
|
@@ -804,6 +953,12 @@ Push your changes with 'emit push <file>' or discard them before pulling.
|
|
|
804
953
|
|
|
805
954
|
**Solution**: Run `emit pull` or `sync_pull` (taxonomies are synced automatically), or use `emit pull-taxonomies` standalone to refresh your local category and tag list. Preflight Guard G4 will guide you with valid options.
|
|
806
955
|
|
|
956
|
+
### Q: `[NETWORK_ERROR]`: Network request failed (`fetch failed`)
|
|
957
|
+
|
|
958
|
+
**Cause**: The configured origin URL is unreachable due to DNS resolution failure, offline network/VPN state, or an incorrect origin in `~/.config/emit/profiles.yaml` or `.emit/config.json`.
|
|
959
|
+
|
|
960
|
+
**Solution**: Verify the `origin` URL reported in `Endpoint:` and check `emit profile list` / `.emit/config.json`. If `.emit/config.json` and your active profile point to different origins, Emit prints an origin mismatch warning to help you reconcile them.
|
|
961
|
+
|
|
807
962
|
---
|
|
808
963
|
|
|
809
964
|
## 🧪 Testing & Quality Assurance
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"new.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/new.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGzC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,kBAAkB,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"new.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/new.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGzC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,kBAAkB,GAAG,IAAI,CAyC7F"}
|
package/dist/cli/commands/new.js
CHANGED
|
@@ -24,10 +24,11 @@ import { getLocalTaxonomiesSummary, formatTaxonomiesHint } from '../../sync/taxo
|
|
|
24
24
|
export function registerNewCommand(program, resolveContext) {
|
|
25
25
|
program
|
|
26
26
|
.command('new <slug>')
|
|
27
|
-
.description('Create a new draft article template with an auto-generated ULID')
|
|
27
|
+
.description('Create a new draft article template with an auto-generated ULID and taxonomy hints')
|
|
28
28
|
.option('-t, --title <title>', 'Article title')
|
|
29
29
|
.option('-c, --collection <collection>', 'Target collection (defaults to "posts")')
|
|
30
30
|
.option('-l, --locale <locale>', 'Article locale (defaults to "ja")')
|
|
31
|
+
.option('-b, --byline <slug>', 'Author byline slug (defaults to first cached non-guest author by slug)')
|
|
31
32
|
.option('-p, --profile <name>', 'Explicitly use a specific connection profile')
|
|
32
33
|
.action((slug, options) => {
|
|
33
34
|
const ctx = resolveContext(options.profile);
|
|
@@ -38,6 +39,7 @@ export function registerNewCommand(program, resolveContext) {
|
|
|
38
39
|
title: options.title,
|
|
39
40
|
collection,
|
|
40
41
|
locale,
|
|
42
|
+
byline: options.byline,
|
|
41
43
|
workspaceRoot: ctx.root,
|
|
42
44
|
cache: ctx.cache,
|
|
43
45
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"new.js","sourceRoot":"","sources":["../../../src/cli/commands/new.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAG3F,MAAM,UAAU,kBAAkB,CAAC,OAAgB,EAAE,cAAkC;IACrF,OAAO;SACJ,OAAO,CAAC,YAAY,CAAC;SACrB,WAAW,
|
|
1
|
+
{"version":3,"file":"new.js","sourceRoot":"","sources":["../../../src/cli/commands/new.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAG3F,MAAM,UAAU,kBAAkB,CAAC,OAAgB,EAAE,cAAkC;IACrF,OAAO;SACJ,OAAO,CAAC,YAAY,CAAC;SACrB,WAAW,CACV,oFAAoF,CACrF;SACA,MAAM,CAAC,qBAAqB,EAAE,eAAe,CAAC;SAC9C,MAAM,CAAC,+BAA+B,EAAE,yCAAyC,CAAC;SAClF,MAAM,CAAC,uBAAuB,EAAE,mCAAmC,CAAC;SACpE,MAAM,CACL,qBAAqB,EACrB,wEAAwE,CACzE;SACA,MAAM,CAAC,sBAAsB,EAAE,8CAA8C,CAAC;SAC9E,MAAM,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE;QACxB,MAAM,GAAG,GAAG,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC5C,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC;QACtE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC;QAE1D,MAAM,GAAG,GAAG,qBAAqB,CAAC;YAChC,IAAI;YACJ,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,UAAU;YACV,MAAM;YACN,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,aAAa,EAAE,GAAG,CAAC,IAAI;YACvB,KAAK,EAAE,GAAG,CAAC,KAAK;SACjB,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,uCAAuC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;QAC9D,OAAO,CAAC,GAAG,CAAC,YAAY,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;QAExC,MAAM,UAAU,GAAG,yBAAyB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvD,MAAM,OAAO,GAAG,oBAAoB,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;QAClE,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,CAAC,GAAG,CAAC,KAAK,OAAO,EAAE,CAAC,CAAC;QAC9B,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC,CAAC;QACjE,OAAO,CAAC,GAAG,CAAC,gBAAgB,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACP,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/profile.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/profile.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUzC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0I9D"}
|