emdash-emit 0.0.0-stage โ†’ 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +798 -2
  3. package/dist/bin/emit.d.ts +5 -0
  4. package/dist/bin/emit.d.ts.map +1 -0
  5. package/dist/bin/emit.js +63 -0
  6. package/dist/bin/emit.js.map +1 -0
  7. package/dist/cli/commands/new.d.ts +4 -0
  8. package/dist/cli/commands/new.d.ts.map +1 -0
  9. package/dist/cli/commands/new.js +49 -0
  10. package/dist/cli/commands/new.js.map +1 -0
  11. package/dist/cli/commands/profile.d.ts +3 -0
  12. package/dist/cli/commands/profile.d.ts.map +1 -0
  13. package/dist/cli/commands/profile.js +104 -0
  14. package/dist/cli/commands/profile.js.map +1 -0
  15. package/dist/cli/commands/pull.d.ts +4 -0
  16. package/dist/cli/commands/pull.d.ts.map +1 -0
  17. package/dist/cli/commands/pull.js +157 -0
  18. package/dist/cli/commands/pull.js.map +1 -0
  19. package/dist/cli/commands/push.d.ts +4 -0
  20. package/dist/cli/commands/push.d.ts.map +1 -0
  21. package/dist/cli/commands/push.js +107 -0
  22. package/dist/cli/commands/push.js.map +1 -0
  23. package/dist/cli/commands/status.d.ts +4 -0
  24. package/dist/cli/commands/status.d.ts.map +1 -0
  25. package/dist/cli/commands/status.js +100 -0
  26. package/dist/cli/commands/status.js.map +1 -0
  27. package/dist/cli/context.d.ts +15 -0
  28. package/dist/cli/context.d.ts.map +1 -0
  29. package/dist/cli/context.js +75 -0
  30. package/dist/cli/context.js.map +1 -0
  31. package/dist/client/api.d.ts +19 -0
  32. package/dist/client/api.d.ts.map +1 -0
  33. package/dist/client/api.js +96 -0
  34. package/dist/client/api.js.map +1 -0
  35. package/dist/client/auth.d.ts +48 -0
  36. package/dist/client/auth.d.ts.map +1 -0
  37. package/dist/client/auth.js +165 -0
  38. package/dist/client/auth.js.map +1 -0
  39. package/dist/client/error.d.ts +22 -0
  40. package/dist/client/error.d.ts.map +1 -0
  41. package/dist/client/error.js +57 -0
  42. package/dist/client/error.js.map +1 -0
  43. package/dist/client/http.d.ts +31 -0
  44. package/dist/client/http.d.ts.map +1 -0
  45. package/dist/client/http.js +97 -0
  46. package/dist/client/http.js.map +1 -0
  47. package/dist/client/lock.d.ts +40 -0
  48. package/dist/client/lock.d.ts.map +1 -0
  49. package/dist/client/lock.js +104 -0
  50. package/dist/client/lock.js.map +1 -0
  51. package/dist/client/mapper.d.ts +37 -0
  52. package/dist/client/mapper.d.ts.map +1 -0
  53. package/dist/client/mapper.js +142 -0
  54. package/dist/client/mapper.js.map +1 -0
  55. package/dist/client/portable-text.d.ts +94 -0
  56. package/dist/client/portable-text.d.ts.map +1 -0
  57. package/dist/client/portable-text.js +428 -0
  58. package/dist/client/portable-text.js.map +1 -0
  59. package/dist/client/schemas.d.ts +1928 -0
  60. package/dist/client/schemas.d.ts.map +1 -0
  61. package/dist/client/schemas.js +123 -0
  62. package/dist/client/schemas.js.map +1 -0
  63. package/dist/db/cache.d.ts +60 -0
  64. package/dist/db/cache.d.ts.map +1 -0
  65. package/dist/db/cache.js +303 -0
  66. package/dist/db/cache.js.map +1 -0
  67. package/dist/index.d.ts +24 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +45 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/mcp/context.d.ts +14 -0
  72. package/dist/mcp/context.d.ts.map +1 -0
  73. package/dist/mcp/context.js +23 -0
  74. package/dist/mcp/context.js.map +1 -0
  75. package/dist/mcp/server.d.ts +12 -0
  76. package/dist/mcp/server.d.ts.map +1 -0
  77. package/dist/mcp/server.js +106 -0
  78. package/dist/mcp/server.js.map +1 -0
  79. package/dist/mcp/tools/profile.d.ts +6 -0
  80. package/dist/mcp/tools/profile.d.ts.map +1 -0
  81. package/dist/mcp/tools/profile.js +81 -0
  82. package/dist/mcp/tools/profile.js.map +1 -0
  83. package/dist/mcp/tools/sync-diff.d.ts +4 -0
  84. package/dist/mcp/tools/sync-diff.d.ts.map +1 -0
  85. package/dist/mcp/tools/sync-diff.js +115 -0
  86. package/dist/mcp/tools/sync-diff.js.map +1 -0
  87. package/dist/mcp/tools/sync-new.d.ts +4 -0
  88. package/dist/mcp/tools/sync-new.d.ts.map +1 -0
  89. package/dist/mcp/tools/sync-new.js +54 -0
  90. package/dist/mcp/tools/sync-new.js.map +1 -0
  91. package/dist/mcp/tools/sync-pull.d.ts +4 -0
  92. package/dist/mcp/tools/sync-pull.d.ts.map +1 -0
  93. package/dist/mcp/tools/sync-pull.js +180 -0
  94. package/dist/mcp/tools/sync-pull.js.map +1 -0
  95. package/dist/mcp/tools/sync-push.d.ts +4 -0
  96. package/dist/mcp/tools/sync-push.d.ts.map +1 -0
  97. package/dist/mcp/tools/sync-push.js +105 -0
  98. package/dist/mcp/tools/sync-push.js.map +1 -0
  99. package/dist/mcp/tools/sync-status.d.ts +4 -0
  100. package/dist/mcp/tools/sync-status.d.ts.map +1 -0
  101. package/dist/mcp/tools/sync-status.js +45 -0
  102. package/dist/mcp/tools/sync-status.js.map +1 -0
  103. package/dist/sync/bylines.d.ts +23 -0
  104. package/dist/sync/bylines.d.ts.map +1 -0
  105. package/dist/sync/bylines.js +112 -0
  106. package/dist/sync/bylines.js.map +1 -0
  107. package/dist/sync/diff.d.ts +16 -0
  108. package/dist/sync/diff.d.ts.map +1 -0
  109. package/dist/sync/diff.js +68 -0
  110. package/dist/sync/diff.js.map +1 -0
  111. package/dist/sync/media.d.ts +37 -0
  112. package/dist/sync/media.d.ts.map +1 -0
  113. package/dist/sync/media.js +191 -0
  114. package/dist/sync/media.js.map +1 -0
  115. package/dist/sync/merge.d.ts +36 -0
  116. package/dist/sync/merge.d.ts.map +1 -0
  117. package/dist/sync/merge.js +112 -0
  118. package/dist/sync/merge.js.map +1 -0
  119. package/dist/sync/new.d.ts +24 -0
  120. package/dist/sync/new.d.ts.map +1 -0
  121. package/dist/sync/new.js +75 -0
  122. package/dist/sync/new.js.map +1 -0
  123. package/dist/sync/orchestrator.d.ts +55 -0
  124. package/dist/sync/orchestrator.d.ts.map +1 -0
  125. package/dist/sync/orchestrator.js +222 -0
  126. package/dist/sync/orchestrator.js.map +1 -0
  127. package/dist/sync/preflight.d.ts +25 -0
  128. package/dist/sync/preflight.d.ts.map +1 -0
  129. package/dist/sync/preflight.js +128 -0
  130. package/dist/sync/preflight.js.map +1 -0
  131. package/dist/sync/pull.d.ts +67 -0
  132. package/dist/sync/pull.d.ts.map +1 -0
  133. package/dist/sync/pull.js +405 -0
  134. package/dist/sync/pull.js.map +1 -0
  135. package/dist/sync/push.d.ts +33 -0
  136. package/dist/sync/push.d.ts.map +1 -0
  137. package/dist/sync/push.js +432 -0
  138. package/dist/sync/push.js.map +1 -0
  139. package/dist/sync/status.d.ts +24 -0
  140. package/dist/sync/status.d.ts.map +1 -0
  141. package/dist/sync/status.js +85 -0
  142. package/dist/sync/status.js.map +1 -0
  143. package/dist/sync/taxonomies.d.ts +49 -0
  144. package/dist/sync/taxonomies.d.ts.map +1 -0
  145. package/dist/sync/taxonomies.js +220 -0
  146. package/dist/sync/taxonomies.js.map +1 -0
  147. package/dist/utils/config.d.ts +18 -0
  148. package/dist/utils/config.d.ts.map +1 -0
  149. package/dist/utils/config.js +79 -0
  150. package/dist/utils/config.js.map +1 -0
  151. package/dist/utils/frontmatter.d.ts +115 -0
  152. package/dist/utils/frontmatter.d.ts.map +1 -0
  153. package/dist/utils/frontmatter.js +147 -0
  154. package/dist/utils/frontmatter.js.map +1 -0
  155. package/dist/utils/naming.d.ts +28 -0
  156. package/dist/utils/naming.d.ts.map +1 -0
  157. package/dist/utils/naming.js +94 -0
  158. package/dist/utils/naming.js.map +1 -0
  159. package/dist/utils/profile.d.ts +72 -0
  160. package/dist/utils/profile.d.ts.map +1 -0
  161. package/dist/utils/profile.js +180 -0
  162. package/dist/utils/profile.js.map +1 -0
  163. package/dist/utils/version.d.ts +5 -0
  164. package/dist/utils/version.d.ts.map +1 -0
  165. package/dist/utils/version.js +43 -0
  166. package/dist/utils/version.js.map +1 -0
  167. package/dist/utils/workspace.d.ts +22 -0
  168. package/dist/utils/workspace.d.ts.map +1 -0
  169. package/dist/utils/workspace.js +74 -0
  170. package/dist/utils/workspace.js.map +1 -0
  171. package/package.json +68 -4
package/README.md CHANGED
@@ -1,3 +1,799 @@
1
- # Temporary Holding Version
1
+ # Emit (`emdash-emit`)
2
+
3
+ [![CI](https://github.com/aroooy/emdash-emit/actions/workflows/ci.yml/badge.svg)](https://github.com/aroooy/emdash-emit/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/emdash-emit.svg)](https://www.npmjs.com/package/emdash-emit)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D22.0.0-brightgreen.svg)](https://nodejs.org/)
7
+
8
+ **Emit** is an AI-first local workspace and bidirectional synchronization tool for [EmDash](https://github.com/aroooy/emdash) (Cloudflare Workers + D1). It provides a full-featured CLI and a Model Context Protocol (MCP) server designed for autonomous coding agents (Antigravity, Claude Code, Cursor) and human writers.
9
+
10
+ ---
11
+
12
+ ## ๐Ÿ“‘ Table of Contents
13
+
14
+ - [Overview & Architecture Philosophy](#-overview--architecture-philosophy)
15
+ - [The Authoring Desk Philosophy & Non-Goals](#1-the-authoring-desk-philosophy--non-goals)
16
+ - [The Sliding Window "Staging Desk" Model](#2-the-sliding-window-staging-desk-model)
17
+ - [Unpushed Edits Safeguard](#3-unpushed-edits-safeguard)
18
+ - [Pure TypeScript & Zero Native Builds](#4-pure-typescript--zero-native-builds)
19
+ - [Distributed Mutex Lock & 3-Way Merge](#5-distributed-mutex-lock--3-way-merge)
20
+ - [๐ŸŒŸ Key Features](#-key-features)
21
+ - [๐Ÿ“ฆ Installation](#-installation)
22
+ - [๐Ÿš€ Initial Setup & Profiles](#-initial-setup--profiles)
23
+ - [Register a Profile](#1-register-a-profile)
24
+ - [Initialize a Workspace](#2-initialize-a-workspace)
25
+ - [Multi-Site Management](#3-multi-site-management)
26
+ - [Recommended `.gitignore`](#4-recommended-gitignore)
27
+ - [๐Ÿ“‚ Directory Structure & Data Specifications](#-directory-structure--data-specifications)
28
+ - [๐Ÿ“ Markdown & YAML Frontmatter Specification](#-markdown--yaml-frontmatter-specification)
29
+ - [Frontmatter Schema](#frontmatter-schema)
30
+ - [Critical Content Rules](#critical-content-rules)
31
+ - [EmDash Custom Block Preservation](#emdash-custom-block-preservation)
32
+ - [Media Management & Upload Workflow](#media-management--upload-workflow)
33
+ - [๐Ÿค– The Golden Path for AI & Humans (4-Step Cycle)](#-the-golden-path-for-ai--humans-4-step-cycle)
34
+ - [The 4-Step Authoring Cycle](#the-4-step-authoring-cycle)
35
+ - [Preflight Safety Guardrails (G1โ€“G9)](#preflight-safety-guardrails-g1g9)
36
+ - [๐Ÿ›  CLI Command Reference](#-cli-command-reference)
37
+ - [`emit init`](#emit-init)
38
+ - [`emit profile`](#emit-profile)
39
+ - [`emit new`](#emit-new)
40
+ - [`emit pull`](#emit-pull)
41
+ - [`emit push`](#emit-push)
42
+ - [`emit status`](#emit-status)
43
+ - [`emit diff`](#emit-diff)
44
+ - [`emit pull-taxonomies`](#emit-pull-taxonomies)
45
+ - [`emit pull-media`](#emit-pull-media)
46
+ - [`emit guide`](#emit-guide)
47
+ - [`emit mcp`](#emit-mcp)
48
+ - [๐Ÿค– Model Context Protocol (MCP) Support](#-model-context-protocol-mcp-support)
49
+ - [Setup Guide](#setup-guide)
50
+ - [Available MCP Tools](#available-mcp-tools)
51
+ - [Autonomous Agent Workflows](#autonomous-agent-workflows)
52
+ - [๐Ÿ”’ Distributed Concurrency & 3-Way Merge](#-distributed-concurrency--3-way-merge)
53
+ - [๐Ÿ”ง Troubleshooting & FAQ](#-troubleshooting--faq)
54
+ - [๐Ÿงช Testing & Quality Assurance](#-testing--quality-assurance)
55
+ - [๐Ÿ“„ License & Copyright](#-license--copyright)
56
+
57
+ ---
58
+
59
+ ## ๐Ÿ’ก Overview & Architecture Philosophy
60
+
61
+ EmDash is a modern serverless headless CMS built on Cloudflare Workers and D1. While Cloudflare D1 provides blazing-fast edge performance, authoring long-form blog posts and articles directly in a web dashboard can be cumbersome.
62
+
63
+ **Emit** bridges this gap. It turns any local directory into a Git-like local authoring workspace, allowing you and AI coding agents to edit Markdown files using your favorite editors (VS Code, Neovim, Cursor, Antigravity) and seamlessly sync them with EmDash.
64
+
65
+ ```text
66
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
67
+ โ”‚ Local Workspace โ”‚ emit push / pull โ”‚ Remote EmDash Server โ”‚
68
+ โ”‚ posts/draft/*.md โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ Cloudflare Workers โ”‚
69
+ โ”‚ posts/published/*.md โ”‚ (AST + Frontmatter)โ”‚ Cloudflare D1 SQL โ”‚
70
+ โ”‚ .emit/cache.db โ”‚ โ”‚ Cloudflare R2 Media โ”‚
71
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
72
+
73
+ ```
74
+
75
+ ### 1. The Authoring Desk Philosophy & Non-Goals
76
+
77
+ To prevent user expectations from misaligning with the tool's purpose, it is important to clarify **what Emit is and what it is not**:
78
+
79
+ * **What Emit IS (Core Goal)**:
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.
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.
85
+
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
+
88
+ > [!NOTE]
89
+ > **Can the sync limit be adjusted?**
90
+ > Yes. While `--all` is intentionally omitted from the CLI to protect the sliding window discipline, you can customize the window size by setting `postsSyncLimit: 300` (or any desired number) in `.emit/config.json`. Emit's internal pagination engine will automatically batch-fetch up to that configured limit.
91
+
92
+ ### 2. The Sliding Window "Staging Desk" Model
93
+
94
+ Traditional synchronization tools attempt to download the entire history of a site. If your blog has thousands of articles, local directories become cluttered, Ripgrep/VSCode search slows down, and AI context windows get overwhelmed with irrelevant historical data.
95
+
96
+ Emit adopts a **bounded sliding window**:
97
+
98
+ * It maintains an active sliding window of recent posts (default: **latest 100 posts**).
99
+ * When older posts fall outside the sliding window during `emit pull`, Emit cleanly **prunes them from disk**, keeping your local repository lean, fast, and organized.
100
+ * Any pruned article can be pulled back into the workspace at any time with `emit pull <id>`.
101
+
102
+ ### 3. Unpushed Edits Safeguard
103
+
104
+ What happens if an article with unpushed local modifications is pushed out of the sliding window?
105
+ Emit guarantees **zero data loss**:
106
+
107
+ * If an article has local modifications not yet pushed to the remote server, Emit's pruning engine **aborts the pull immediately** with a `SyncPruneConflictError`.
108
+ * It lists the exact unpushed file and informs the user to either push the changes (`emit push <file>`) or discard them before proceeding.
109
+
110
+ ### 4. Pure TypeScript & Zero Native Builds
111
+
112
+ Emit has **zero C++ native dependencies** (`node-gyp`).
113
+ By leveraging Node.js 22's built-in `node:sqlite` module (`DatabaseSync`), Emit installs in seconds on macOS, Linux, and Windows without requiring system build tools or Python.
114
+
115
+ ### 5. Distributed Mutex Lock & 3-Way Merge
116
+
117
+ To prevent concurrent race conditions when multiple authors or AI agents collaborate:
118
+
119
+ * Emit acquires a distributed 7-minute lease via `POST /api/v1/content/lock` before writing to remote articles, with automatic heartbeats and `try/finally` release guarantees.
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
+
122
+ ---
123
+
124
+ ## ๐ŸŒŸ Key Features
125
+
126
+ * **AI-First Authoring Workflow**: Built specifically for autonomous agents and humans to draft, inspect, and sync local Markdown files safely.
127
+ * **Sliding Window Synchronization**: Automatically manages an active sliding window of recent posts (default: latest 100), pruning older cached entries from disk.
128
+ * **Unpushed Edits Safeguard**: Detects unpushed local changes outside the sync window and aborts sync safely before any files can be deleted.
129
+ * **Git-Style Workspace Encapsulation**: Houses local SQLite cache (`node:sqlite`) and workspace configuration neatly inside a `.emit/` hidden folder.
130
+ * **Multi-Site Profile Management**: Manage multiple EmDash sites and credentials in `~/.config/emit/profiles.yaml` with automatic workspace directory detection.
131
+ * **Hybrid 3-Way Merge Engine**: Resolves concurrent changes between remote and local versions like Git, independently merging YAML frontmatter and Markdown body.
132
+ * **Distributed Mutual Exclusion Lock**: Acquires 7-minute leases via `POST /lock` with `try/finally` release semantics to eliminate race conditions during publishing.
133
+ * **Opaque Block Preservation**: Preserves EmDash custom blocks (e.g., `core:gallery`) losslessly across AST roundtrips via `<!--ec:block ... -->` fences.
134
+ * **Taxonomy & Media Synchronization**: Sync category/tag YAML schemas and download media assets with metadata.
135
+ * **Zero Native Dependencies**: Completely eliminates native C++ builds (`node-gyp`) by leveraging Node.js 22+ built-in `node:sqlite`.
136
+
137
+ ---
138
+
139
+ ## ๐Ÿ“ฆ Installation
140
+
141
+ Emit requires **Node.js v22.0.0** or higher.
142
+
143
+ ### Method A: Global Installation via npm (Recommended)
144
+
145
+ ```bash
146
+ npm install -g emdash-emit
147
+
148
+ # Verify installation
149
+ emit --version
150
+
151
+ ```
152
+
153
+ ### Method B: Temporary Execution via npx
154
+
155
+ Run commands without installing globally:
156
+
157
+ ```bash
158
+ npx emdash-emit status
159
+ npx emdash-emit pull
160
+
161
+ ```
162
+
163
+ ### Method C: Local Development Link
164
+
165
+ ```bash
166
+ git clone https://github.com/aroooy/emdash-emit.git
167
+ cd emdash-emit
168
+ npm install
169
+ npm run build
170
+ npm install -g .
171
+
172
+ ```
173
+
174
+ ---
175
+
176
+ ## ๐Ÿš€ Initial Setup & Profiles
177
+
178
+ ### 1. Register a Profile
179
+
180
+ Add your EmDash instance credentials to the global profile registry:
181
+
182
+ ```bash
183
+ emit profile add my-blog \
184
+ --origin https://your-blog.example.com \
185
+ --token <your-personal-access-token> \
186
+ --workspace /path/to/local/blog-repo
187
+ ```
188
+
189
+ * `--origin`: The base URL of your EmDash Cloudflare Workers instance.
190
+ * `--token`: Your Personal Access Token (PAT) generated from the EmDash dashboard.
191
+ * `--workspace`: The local directory where articles and assets will be synchronized.
192
+
193
+ > [!NOTE]
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
+
196
+ ### 2. Initialize a Workspace
197
+
198
+ Navigate to your local repository directory and initialize it:
199
+
200
+ ```bash
201
+ cd /path/to/local/blog-repo
202
+ emit init
203
+ ```
204
+
205
+ This creates the `.emit/` directory with a default `config.json` and initializes the local baseline cache SQLite database (`.emit/cache.db`).
206
+
207
+ ### 3. Multi-Site Management (Directory-Driven & Zero Friction)
208
+
209
+ Emit is designed from the ground up for authors who manage multiple blogs or environments (e.g. personal blog, company blog, staging site).
210
+
211
+ Because each profile records its own `--workspace` directory, **you never have to manually switch global "active" sites**. Emit resolves the target site automatically based on where you are:
212
+
213
+ ```bash
214
+ # Register personal blog
215
+ emit profile add personal --origin https://personal.example.com --token <pat-1> --workspace ~/blogs/personal
216
+
217
+ # Register company blog
218
+ emit profile add work --origin https://blog.company.example.com --token <pat-2> --workspace ~/blogs/work
219
+
220
+ # List all registered profiles
221
+ emit profile list
222
+ ```
223
+
224
+ **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`.
226
+ 2. **Explicit Profile Flag**: You can explicitly specify `--profile <name>` (or pass `profile: "<name>"` in MCP) from any directory to override automatic detection.
227
+ 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
+
229
+ ### 4. Recommended `.gitignore`
230
+
231
+ When tracking your local blog repository with Git, use the following recommended `.gitignore` configuration:
232
+
233
+ ```gitignore
234
+ # Emit Local Cache Database (Strictly exclude binary SQLite DBs)
235
+ .emit/cache.db*
236
+ .emit/*.db*
237
+ *.db
238
+ *.db-wal
239
+ *.db-shm
240
+
241
+ # OS & Editor temporary files
242
+ .DS_Store
243
+ Thumbs.db
244
+ Temp/
245
+ tmp/
246
+
247
+ # Optional: Media binaries (Uncomment if you prefer not to commit large images to Git)
248
+ # media/*.jpg
249
+ # media/*.png
250
+ # media/*.webp
251
+ # media/*.avif
252
+ # media/*.mp4
253
+
254
+ ```
255
+
256
+ > [!TIP]
257
+ > * `.emit/cache.db*` **must be excluded** from Git because it is a machine-local SQLite tracking database.
258
+ > * `.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
+ >
262
+
263
+ ---
264
+
265
+ ## ๐Ÿ“‚ Directory Structure & Data Specifications
266
+
267
+ When synchronized, your local workspace will be organized as follows:
268
+
269
+ ```text
270
+ your-blog/
271
+ โ”œโ”€โ”€ .emit/ # [Hidden System Directory - Do not edit manually]
272
+ โ”‚ โ”œโ”€โ”€ config.json # Workspace configuration (origin, postsSyncLimit, etc.)
273
+ โ”‚ โ””โ”€โ”€ cache.db # SQLite baseline tracking & hash database
274
+ โ”œโ”€โ”€ categories/ # Category definition files
275
+ โ”‚ โ”œโ”€โ”€ tech.yaml
276
+ โ”‚ โ””โ”€โ”€ lifestyle.yaml
277
+ โ”œโ”€โ”€ tags/ # Tag definition files
278
+ โ”‚ โ”œโ”€โ”€ typescript.yaml
279
+ โ”‚ โ””โ”€โ”€ ai.yaml
280
+ โ”œโ”€โ”€ media/ # Media files and metadata
281
+ โ”‚ โ”œโ”€โ”€ 01M6ABCDEF.jpg
282
+ โ”‚ โ””โ”€โ”€ 01M6ABCDEF.jpg.yaml
283
+ โ””โ”€โ”€ posts/ # Local Markdown Articles
284
+ โ”œโ”€โ”€ draft/ # Draft articles (<yyyyMMdd>_<ulid>-<slug>.md)
285
+ โ”‚ โ””โ”€โ”€ 20261104_01M6...-my-new-post.md
286
+ โ””โ”€โ”€ published/ # Live published articles (<yyyyMMdd>_<ulid>-<slug>.md)
287
+ โ””โ”€โ”€ 20261015_01M5...-getting-started.md
288
+
289
+ ```
290
+
291
+ ---
292
+
293
+ ## ๐Ÿ“ Markdown & YAML Frontmatter Specification
294
+
295
+ Each article is stored as a Markdown file with a standard YAML frontmatter header.
296
+
297
+ ### Frontmatter Schema
298
+
299
+ ```yaml
300
+ ---
301
+ id: 01M6ABCDEF1234567890123456 # EmDash canonical ULID (auto-generated; DO NOT EDIT)
302
+ title: "Building AI-Powered CMS Tools"
303
+ slug: "building-ai-powered-cms-tools"
304
+ status: draft # Status: "draft" or "published"
305
+ publishedAt: null # ISO 8601 timestamp (null for drafts)
306
+ updatedAt: "2026-11-04T12:00:00.000Z"
307
+ excerpt: "A deep dive into building autonomous content workflows with EmDash and Emit."
308
+ featuredImage: "01M6MEDIA123456" # Media ULID or null
309
+ categories: # Slugs matching definitions in categories/*.yaml
310
+ - tech
311
+ tags: # Slugs matching definitions in tags/*.yaml
312
+ - typescript
313
+ - ai
314
+ customFields: # Optional custom field definitions
315
+ canonicalUrl: ""
316
+ ---
317
+
318
+ Markdown body begins here...
319
+
320
+ ```
321
+
322
+ ### Critical Content Rules
323
+
324
+ 1. **Do Not Duplicate the Title in the Body**:
325
+ - The article title is managed by the `title:` field in the frontmatter.
326
+ - **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
+ 2. **Taxonomy Slugs**:
328
+ - Items under `categories:` and `tags:` must match existing slug names from `categories/` and `tags/`.
329
+ - Never invent arbitrary IDs or invalid names; pull taxonomies with `emit pull-taxonomies` to see available options.
330
+ 3. **Status Transitions**:
331
+ - To publish a draft, update `status: published` in the frontmatter and execute `emit push <filePath> --allow-publish`.
332
+ - Emit automatically updates the remote state and moves the file from `posts/draft/` to `posts/published/`.
333
+ - **Never move or rename files manually using `mv`**.
334
+
335
+ ### EmDash Custom Block Preservation
336
+
337
+ EmDash supports rich visual blocks (e.g., galleries, callouts, audio players). When converted from EmDash's internal AST to Markdown, Emit wraps non-standard blocks in opaque comment fences:
338
+
339
+ ```html
340
+ <!--ec:block type="core:gallery" id="01M6XYZ..." data="{\"columns\":3,\"images\":[...]}" -->
341
+ <!--/ec:block-->
342
+
343
+ ```
344
+
345
+ * **Rule**: Do not modify or delete these comment fences unless you intentionally want to remove the block. Emit preserves them losslessly on round-trips.
346
+
347
+ ### Media Management & Upload Workflow
348
+
349
+ EmDash stores media files (images, documents) in Cloudflare R2 via multipart upload endpoints.
350
+
351
+ **How to add and use images in your articles:**
352
+
353
+ 1. **Upload New Images**: Upload images via the **EmDash Admin Web Dashboard** (`/admin/media`) or via the official EmDash MCP tool (`media_upload`). This generates a canonical ULID and uploads the asset to Cloudflare R2.
354
+ 2. **Sync Media Locally**: Run `emit pull-media` in your workspace. This downloads the images and companion `.yaml` metadata files into `media/`.
355
+ 3. **Embed in Articles**:
356
+ - For featured hero images: set `featuredImage: "01M6..."` in frontmatter.
357
+ - For inline images in body: use standard Markdown syntax with remote URLs: `![Alt text](https://your-blog.example.com/api/v1/media/01M6.../file.jpg)`.
358
+
359
+ > [!WARNING]
360
+ > **No Local Image Paths in Markdown**: Preflight Guard **G5** strictly forbids pushing articles containing local filesystem paths like `![pic](./my-image.png)`. Images must be uploaded to Cloudflare R2 before publishing to prevent broken images in production.
361
+
362
+ ---
363
+
364
+ ## ๐Ÿค– The Golden Path for AI & Humans (4-Step Cycle)
365
+
366
+ Whether you are a human developer or an autonomous AI agent (Antigravity, Claude Code, Cursor), always follow the **4-Step Cycle**:
367
+
368
+ ```text
369
+ [1. CHECK] Inspect local workspace state โ”€โ”€โ–บ emit status
370
+ โ”‚
371
+ [2. PULL] Sync remote updates safely โ”€โ”€โ–บ emit pull
372
+ โ”‚
373
+ [3. EDIT] Write & refine Markdown directly โ”€โ”€โ–บ posts/draft/*.md
374
+ โ”‚
375
+ [4. PUSH] Push changes to remote โ”€โ”€โ–บ emit push <filePath>
376
+
377
+ ```
378
+
379
+ ### The 4-Step Authoring Cycle
380
+
381
+ 1. **CHECK (`emit status`)**: Check your workspace status to see synced, modified, and untracked files.
382
+ 2. **PULL (`emit pull`)**: Synchronize the latest sliding window from the server into your local workspace.
383
+ 3. **EDIT (Direct Editing)**:
384
+ * For new posts: run `emit new <slug> --title "My Title"` to scaffold a canonical draft.
385
+ * Edit the Markdown body and YAML frontmatter in `posts/draft/` or `posts/published/`.
386
+
387
+ 4. **PUSH (`emit push`)**:
388
+ * To push a draft: `emit push posts/draft/<filename>.md`
389
+ * To publish live: update `status: published` and run `emit push posts/draft/<filename>.md --allow-publish`.
390
+
391
+ ### Preflight Safety Guardrails (G1โ€“G9)
392
+
393
+ To ensure autonomous AI agents and automated scripts cannot corrupt workspace state or accidentally publish unintended content, Emit enforces 9 strict preflight guardrails before executing any write operation:
394
+
395
+ | Guard | Code Name | Description | Severity |
396
+ | --- | --- | --- | --- |
397
+ | **G1** | `SINGLE_TARGET_GUARD` | Enforces explicit target (`filePath`, `id`, or `--all`) for push/pull; forbids blind bulk push without inspection | Error |
398
+ | **G2** | `ID_MISMATCH_ERROR` | Verifies that frontmatter `id` exactly matches the filename ULID; forbids manual `mv` renames | Error |
399
+ | **G3** | `UNINTENDED_PUBLISH_GUARD` | Rejects promoting `draft` to `published` unless `--allow-publish` (`allowPublish: true`) is explicitly given | Error |
400
+ | **G4** | `INVALID_TAXONOMY_ERROR` | Validates category and tag slugs against local YAML definitions to stop AI taxonomy hallucination | Error |
401
+ | **G5** | `LOCAL_IMAGE_DETECTED` | Detects unresolved local image paths (`![alt](./path.png)`); requires remote R2 URLs | Error |
402
+ | **G6** | `UNRESOLVED_CONFLICT_MARKERS` | Scans for Git 3-way merge conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) and blocks push | Error |
403
+ | **G7** | `TITLE_DUPLICATION_AUTOTRIM` | Detects redundant `# Title` headings at the top of the body and automatically trims them safely | Warning |
404
+ | **G8** | `UNSUPPORTED_SYNTAX_WARNING` | Warns if unsupported block HTML or incompatible constructs are detected in Markdown | Warning |
405
+ | **G9** | `MODIFIED_OPAQUE_BLOCK` | Verifies that `<!--ec:block ... -->` opaque fences have not been tampered with or corrupted | Error |
406
+
407
+ ---
408
+
409
+ ## ๐Ÿ›  CLI Command Reference
410
+
411
+ ### `emit init`
412
+
413
+ Initializes the current directory as an Emit local workspace.
414
+
415
+ ```bash
416
+ emit init [--origin <url>]
417
+
418
+ ```
419
+
420
+ * `--origin <url>`: Optional EmDash origin URL. If omitted, uses the active profile.
421
+
422
+ **Example Output:**
423
+
424
+ ```text
425
+ โœ… Initialized Emit workspace at: /Volumes/ext/github/my-blog
426
+ Config: /Volumes/ext/github/my-blog/.emit/config.json
427
+ Database: /Volumes/ext/github/my-blog/.emit/cache.db
428
+
429
+ ```
430
+
431
+ ---
432
+
433
+ ### `emit profile`
434
+
435
+ Manage connection profiles across multiple EmDash instances.
436
+
437
+ ```bash
438
+ # List all configured profiles and their workspace directories
439
+ emit profile list
440
+
441
+ # Add or update a profile
442
+ emit profile add <name> --origin <url> [--token <pat>] [--workspace <path>]
443
+
444
+ # Show currently resolved profile for this directory
445
+ emit profile current
446
+
447
+ # Remove a profile
448
+ emit profile remove <name>
449
+
450
+ # (Optional) Set a fallback default profile for when operating outside any workspace
451
+ emit profile use <name>
452
+ ```
453
+
454
+ ---
455
+
456
+ ### `emit new`
457
+
458
+ Scaffolds a new draft article with a canonical 26-character ULID timestamp.
459
+
460
+ ```bash
461
+ emit new <slug> [-t, --title <title>] [-c, --collection <collection>] [-l, --locale <locale>]
462
+
463
+ ```
464
+
465
+ * `<slug>`: URL slug for the article (e.g., `modern-web-architecture`).
466
+ * `-t, --title <title>`: Human-readable article title.
467
+ * `-c, --collection <name>`: Collection name (defaults to `posts`).
468
+ * `-l, --locale <locale>`: Locale code (defaults to `ja` or config).
469
+
470
+ **Example:**
471
+
472
+ ```bash
473
+ emit new ai-workflow-guide --title "AI Workflow Guide"
474
+
475
+ ```
476
+
477
+ **Output:**
478
+
479
+ ```text
480
+ โœ… Created new article scaffold (ID: 01M6ABCDEF1234567890123456)
481
+ Path: posts/draft/20261104_01M6ABCDEF1234567890123456-ai-workflow-guide.md
482
+
483
+ Next: Write your content in Markdown, then run:
484
+ emit push posts/draft/20261104_01M6ABCDEF1234567890123456-ai-workflow-guide.md
485
+
486
+ ```
487
+
488
+ ---
489
+
490
+ ### `emit pull`
491
+
492
+ Synchronizes remote articles into the local workspace sliding window.
493
+
494
+ ```bash
495
+ emit pull [id] [options]
496
+
497
+ ```
498
+
499
+ * `[id]`: Optional article ID or ULID. If specified, pulls only that specific article (even if outside the window).
500
+ * `-s, --status <draft|published|all>`: Filter by publication status (default: `all`).
501
+ * `-c, --collection <collection>`: Collection name (default: `posts`).
502
+ * `--no-prune`: Do not prune local articles that have fallen outside the sliding window (escape hatch; not intended for routine use).
503
+ * `--force`: Force overwrite local files and bypass differential hash checking.
504
+ * `--concurrency <n>`: Parallel processing task limit (default: `8`).
505
+
506
+ > [!NOTE]
507
+ > **Why `--no-prune` is rarely needed:**
508
+ > Automatic pruning is mathematically loss-free: Emit's unpushed edits safeguard strictly aborts the pull if an older article contains local modifications. You only need `--no-prune` in exceptional circumstances, such as temporarily suppressing Git working-tree changes during a quick pull.
509
+
510
+ **Example:**
511
+
512
+ ```bash
513
+ # Pull the latest sliding window (prunes older cached files automatically)
514
+ emit pull
515
+
516
+ # Pull a specific legacy article by ID
517
+ emit pull 01M5XYZ9876543210123456789
518
+
519
+ ```
520
+
521
+ ---
522
+
523
+ ### `emit push`
524
+
525
+ Pushes local markdown article changes to the remote EmDash instance.
526
+
527
+ ```bash
528
+ emit push [file] [options]
529
+
530
+ ```
531
+
532
+ * `[file]`: Relative or absolute path to the Markdown file.
533
+ * `--allow-publish`: Required when promoting an article to `status: published`.
534
+ * `--all`: Pushes all modified files in the workspace.
535
+ * `-p, --profile <name>`: Explicitly target a specific connection profile.
536
+
537
+ **Example:**
538
+
539
+ ```bash
540
+ # Push draft updates
541
+ emit push posts/draft/20261104_01M6...-my-article.md
542
+
543
+ # Publish live to remote
544
+ emit push posts/draft/20261104_01M6...-my-article.md --allow-publish
545
+
546
+ ```
547
+
548
+ ---
549
+
550
+ ### `emit status`
551
+
552
+ Displays the local workspace synchronization status compared against the baseline cache.
553
+
554
+ ```bash
555
+ emit status [-c, --collection <collection>] [-p, --profile <name>]
556
+
557
+ ```
558
+
559
+ **Example Output:**
560
+
561
+ ```text
562
+ --- Workspace Status (posts | origin: https://your-blog.example.com) ---
563
+ ๐ŸŸข [synced ] published posts/published/20261015_01M5...-getting-started.md (Getting Started)
564
+ ๐ŸŸก [modified] draft posts/draft/20261104_01M6...-ai-workflow-guide.md (AI Workflow Guide)
565
+ โšช [untracked] draft posts/draft/20261104_01M6...-notes.md (Draft Notes)
566
+
567
+ Summary: 3 total (1 synced, 1 modified, 1 untracked)
568
+
569
+ ```
570
+
571
+ ---
572
+
573
+ ### `emit diff`
574
+
575
+ Shows line-by-line differences between the working Markdown file and the baseline snapshot.
576
+
577
+ ```bash
578
+ emit diff [filePath]
579
+
580
+ ```
581
+
582
+ ---
583
+
584
+ ### `emit pull-taxonomies`
585
+
586
+ Downloads categories and tags from EmDash and generates local YAML definitions under `categories/` and `tags/`.
587
+
588
+ ```bash
589
+ emit pull-taxonomies
590
+
591
+ ```
592
+
593
+ **Output:**
594
+
595
+ ```text
596
+ Pulling categories and tags from remote...
597
+ โœ… Successfully synced taxonomies: 12 categories, 34 tags.
598
+
599
+ ```
600
+
601
+ ---
602
+
603
+ ### `emit pull-media`
604
+
605
+ Downloads media assets and their companion metadata YAML files into `media/`.
606
+
607
+ ```bash
608
+ emit pull-media [-l, --limit <n>] [--all] [--prune] [--metadata-only]
609
+
610
+ ```
611
+
612
+ * `-l, --limit <n>`: Maximum number of media items to fetch (default: `300`).
613
+ * `--all`: Fetch all media items without limit.
614
+ * `--prune`: Remove local media files that no longer exist on remote.
615
+ * `--metadata-only`: Download metadata YAML files only, skipping large binary downloads.
616
+
617
+ ---
618
+
619
+ ### `emit guide`
620
+
621
+ Prints the concise Golden Path cheat sheet and AI safety rules directly to the console.
622
+
623
+ ```bash
624
+ emit guide
625
+
626
+ ```
627
+
628
+ ---
629
+
630
+ ### `emit mcp`
631
+
632
+ Launches Emit as a Model Context Protocol (MCP) stdio server for AI agents.
633
+
634
+ ```bash
635
+ emit mcp [-w, --workspace <path>]
636
+
637
+ ```
638
+
639
+ ---
640
+
641
+ ## ๐Ÿค– Model Context Protocol (MCP) Support
642
+
643
+ Emit features a first-class **Model Context Protocol (MCP)** server over stdio. This empowers AI assistantsโ€”such as **Google Antigravity**, **Claude Code**, and **Cursor**โ€”to autonomously manage, draft, review, and synchronize EmDash content.
644
+
645
+ ### Setup Guide
646
+
647
+ #### 1. Claude Desktop (`claude_desktop_config.json`)
648
+
649
+ ```json
650
+ {
651
+ "mcpServers": {
652
+ "emit": {
653
+ "command": "emit",
654
+ "args": ["mcp"]
655
+ }
656
+ }
657
+ }
658
+
659
+ ```
660
+
661
+ #### 2. Cursor (`.cursor/mcp.json`)
662
+
663
+ ```json
664
+ {
665
+ "mcpServers": {
666
+ "emit": {
667
+ "command": "npx",
668
+ "args": ["-y", "emdash-emit", "mcp"]
669
+ }
670
+ }
671
+ }
672
+
673
+ ```
674
+
675
+ #### 3. Google Antigravity (`~/.gemini/antigravity/mcp_config.json`)
676
+
677
+ ```json
678
+ {
679
+ "mcpServers": {
680
+ "emit": {
681
+ "command": "emit",
682
+ "args": ["mcp"]
683
+ }
684
+ }
685
+ }
686
+
687
+ ```
688
+
689
+ > **Tip:** When profiles are registered using `emit profile add --workspace <dir>`, the MCP server automatically discovers credentials and paths without requiring secrets in your agent configuration.
690
+
691
+ ### Available MCP Tools
692
+
693
+ | Tool | Purpose | Key Parameters |
694
+ | --- | --- | --- |
695
+ | `sync_status` | Inspect local sync state (`synced`, `modified`, `untracked`) | `collection` |
696
+ | `sync_pull` | Pull remote posts into local sliding window or pull by ID | `id`, `force`, `status`, `noPrune` (escape hatch) |
697
+ | `sync_new` | Scaffold new article with canonical ULID and frontmatter | `slug`, `title`, `locale`, `collection` |
698
+ | `sync_push` | Push local article changes to remote with preflight checks | `filePath`, `id`, `allowPublish` |
699
+ | `sync_diff` | Inspect diff between working file and baseline snapshot | `filePath` |
700
+ | `sync_pull_taxonomies` | Pull remote category and tag definitions into local YAML files | (none) |
701
+ | `sync_pull_media` | Pull remote media files and metadata into `media/` | `limit`, `all`, `prune`, `metadataOnly` |
702
+ | `profile_list` | List all configured connection profiles | (none) |
703
+ | `profile_switch` | Switch active profile for multi-site environments | `name` |
704
+
705
+ ### Autonomous Agent Workflows
706
+
707
+ With Emit connected via MCP, you can give high-level natural language instructions to your AI:
708
+
709
+ * *"Check the workspace status, pull the latest posts, and show me what was changed recently."*
710
+ * *"Draft a new blog post titled 'Next-Gen Serverless Architectures' with slug `serverless-next`, add the `tech` category, and write a comprehensive outline."*
711
+ * *"Review the draft in `posts/draft/...`, proofread the content, and push it to remote as a draft."*
712
+
713
+ ---
714
+
715
+ ## ๐Ÿ”’ Distributed Concurrency & 3-Way Merge
716
+
717
+ ### Distributed Mutex Lock Protocol
718
+
719
+ To eliminate race conditions when multiple users or agents edit the same post:
720
+
721
+ 1. Emit issues a `POST /api/v1/content/lock` request to EmDash before pushing.
722
+ 2. EmDash grants a 7-minute mutual exclusion lease.
723
+ 3. If another author currently holds the lock, Emit aborts with an informative message indicating who owns the lock and when the lease expires.
724
+ 4. Emit guarantees lock release using strict `try/finally` execution.
725
+
726
+ ### Hybrid 3-Way Merge
727
+
728
+ If a remote article was updated after you pulled it:
729
+
730
+ 1. Emit loads the **Base Snapshot** (the state when you last pulled), the **Local Working File**, and the **Remote Server State**.
731
+ 2. **YAML Frontmatter**: Fields are merged key-by-key. Non-conflicting changes are applied automatically.
732
+ 3. **Markdown Body**: Merged line-by-line using `node-diff3`.
733
+ 4. If a conflict occurs, standard Git conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) are injected for human or agent resolution.
734
+
735
+ ---
736
+
737
+ ## ๐Ÿ”ง Troubleshooting & FAQ
738
+
739
+ ### Q: `SyncPruneConflictError`: Pull aborted due to unpushed modifications
740
+
741
+ ```text
742
+ โŒ Cannot prune 1 local article(s) outside the sync window because they contain unpushed changes:
743
+ - posts/draft/20261001_01M4...-old-draft.md
744
+ Push your changes with 'emit push <file>' or discard them before pulling.
745
+
746
+ ```
747
+
748
+ **Cause**: An article that has fallen outside the sliding window (e.g., beyond the latest 100 posts) contains local edits that have not been pushed to the remote server. To prevent accidental data loss, Emit aborts the pull.
749
+
750
+ **Solution**:
751
+
752
+ * If you want to keep the changes: Push them to remote using `emit push posts/draft/...`.
753
+ * If you want to discard local edits: Delete or revert the file, then re-run `emit pull`.
754
+ * If you want to temporarily skip pruning: Run `emit pull --no-prune`.
755
+
756
+ ### Q: `PushValidationError`: Cannot publish article live without `--allow-publish`
757
+
758
+ **Cause**: Frontmatter has `status: published`, but `emit push` was executed without `--allow-publish`.
759
+
760
+ **Solution**: Run `emit push <filePath> --allow-publish`. This safety guardrail prevents accidental live publication.
761
+
762
+ ### Q: Taxonomies not recognized during push
763
+
764
+ **Cause**: Categories or tags in frontmatter do not exist in the remote database.
765
+
766
+ **Solution**: Run `emit pull-taxonomies` to refresh your local category and tag list, and ensure frontmatter values match existing slugs.
767
+
768
+ ---
769
+
770
+ ## ๐Ÿงช Testing & Quality Assurance
771
+
772
+ Emit adheres to high code quality and test-driven development standards:
773
+
774
+ ```bash
775
+ # Run unit test suite (Vitest)
776
+ npm test
777
+
778
+ # Run TypeScript strict typecheck
779
+ npm run typecheck
780
+
781
+ # Verify Prettier code style
782
+ npm run format:check
783
+
784
+ # Auto-format codebase
785
+ npm run format
786
+
787
+ # Run complete CI verification suite
788
+ npm run check
789
+
790
+ ```
791
+
792
+ ---
793
+
794
+ ## ๐Ÿ“„ License & Copyright
795
+
796
+ This project is open-source software licensed under the [MIT License](LICENSE).
797
+
798
+ Copyright (c) 2026 SPORT-SERVICE RSโ˜…R. All rights reserved.
2
799
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.