emdash-emit 0.1.8 → 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.
Files changed (87) hide show
  1. package/README.md +58 -23
  2. package/dist/cli/commands/profile.d.ts.map +1 -1
  3. package/dist/cli/commands/profile.js +67 -13
  4. package/dist/cli/commands/profile.js.map +1 -1
  5. package/dist/cli/commands/pull.d.ts +5 -0
  6. package/dist/cli/commands/pull.d.ts.map +1 -1
  7. package/dist/cli/commands/pull.js +96 -39
  8. package/dist/cli/commands/pull.js.map +1 -1
  9. package/dist/cli/commands/push.d.ts.map +1 -1
  10. package/dist/cli/commands/push.js +16 -1
  11. package/dist/cli/commands/push.js.map +1 -1
  12. package/dist/cli/commands/status.d.ts +19 -0
  13. package/dist/cli/commands/status.d.ts.map +1 -1
  14. package/dist/cli/commands/status.js +69 -3
  15. package/dist/cli/commands/status.js.map +1 -1
  16. package/dist/cli/context.d.ts +35 -0
  17. package/dist/cli/context.d.ts.map +1 -1
  18. package/dist/cli/context.js +112 -10
  19. package/dist/cli/context.js.map +1 -1
  20. package/dist/client/auth.d.ts +1 -0
  21. package/dist/client/auth.d.ts.map +1 -1
  22. package/dist/client/auth.js +1 -0
  23. package/dist/client/auth.js.map +1 -1
  24. package/dist/client/http.d.ts.map +1 -1
  25. package/dist/client/http.js +27 -10
  26. package/dist/client/http.js.map +1 -1
  27. package/dist/mcp/context.d.ts +1 -0
  28. package/dist/mcp/context.d.ts.map +1 -1
  29. package/dist/mcp/server.d.ts +8 -0
  30. package/dist/mcp/server.d.ts.map +1 -1
  31. package/dist/mcp/server.js +52 -13
  32. package/dist/mcp/server.js.map +1 -1
  33. package/dist/mcp/tool-errors.d.ts +11 -0
  34. package/dist/mcp/tool-errors.d.ts.map +1 -0
  35. package/dist/mcp/tool-errors.js +62 -0
  36. package/dist/mcp/tool-errors.js.map +1 -0
  37. package/dist/mcp/tools/profile.d.ts +9 -2
  38. package/dist/mcp/tools/profile.d.ts.map +1 -1
  39. package/dist/mcp/tools/profile.js +49 -36
  40. package/dist/mcp/tools/profile.js.map +1 -1
  41. package/dist/mcp/tools/sync-diff.d.ts.map +1 -1
  42. package/dist/mcp/tools/sync-diff.js +20 -7
  43. package/dist/mcp/tools/sync-diff.js.map +1 -1
  44. package/dist/mcp/tools/sync-new.d.ts.map +1 -1
  45. package/dist/mcp/tools/sync-new.js +35 -22
  46. package/dist/mcp/tools/sync-new.js.map +1 -1
  47. package/dist/mcp/tools/sync-pull.d.ts.map +1 -1
  48. package/dist/mcp/tools/sync-pull.js +101 -72
  49. package/dist/mcp/tools/sync-pull.js.map +1 -1
  50. package/dist/mcp/tools/sync-push.d.ts.map +1 -1
  51. package/dist/mcp/tools/sync-push.js +24 -32
  52. package/dist/mcp/tools/sync-push.js.map +1 -1
  53. package/dist/mcp/tools/sync-status.d.ts.map +1 -1
  54. package/dist/mcp/tools/sync-status.js +55 -4
  55. package/dist/mcp/tools/sync-status.js.map +1 -1
  56. package/dist/sync/orchestrator.d.ts +6 -0
  57. package/dist/sync/orchestrator.d.ts.map +1 -1
  58. package/dist/sync/orchestrator.js +52 -27
  59. package/dist/sync/orchestrator.js.map +1 -1
  60. package/dist/sync/pull.d.ts +47 -0
  61. package/dist/sync/pull.d.ts.map +1 -1
  62. package/dist/sync/pull.js +148 -8
  63. package/dist/sync/pull.js.map +1 -1
  64. package/dist/sync/push.d.ts +2 -0
  65. package/dist/sync/push.d.ts.map +1 -1
  66. package/dist/sync/push.js +30 -3
  67. package/dist/sync/push.js.map +1 -1
  68. package/dist/sync/status.d.ts +11 -1
  69. package/dist/sync/status.d.ts.map +1 -1
  70. package/dist/sync/status.js +40 -6
  71. package/dist/sync/status.js.map +1 -1
  72. package/dist/sync/taxonomies.d.ts.map +1 -1
  73. package/dist/sync/taxonomies.js +39 -4
  74. package/dist/sync/taxonomies.js.map +1 -1
  75. package/dist/utils/config.d.ts +11 -0
  76. package/dist/utils/config.d.ts.map +1 -1
  77. package/dist/utils/config.js +14 -4
  78. package/dist/utils/config.js.map +1 -1
  79. package/dist/utils/frontmatter.d.ts +13 -1
  80. package/dist/utils/frontmatter.d.ts.map +1 -1
  81. package/dist/utils/frontmatter.js +49 -7
  82. package/dist/utils/frontmatter.js.map +1 -1
  83. package/dist/utils/profile.d.ts +3 -3
  84. package/dist/utils/profile.d.ts.map +1 -1
  85. package/dist/utils/profile.js +105 -8
  86. package/dist/utils/profile.js.map +1 -1
  87. package/package.json +1 -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–G11)](#preflight-safety-guardrails-g1g11)
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)
@@ -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 a default `config.json` and initializes the local baseline cache SQLite database (`.emit/cache.db`).
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,7 +225,7 @@ 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
 
@@ -230,7 +233,7 @@ emit profile list
230
233
 
231
234
  Emit resolves authentication credentials using a strict 4-tier priority chain (`src/client/auth.ts`):
232
235
 
233
- 1. **Environment Variables (`EMDASH_TOKEN` or `EMDASH_API_TOKEN`, plus optional `EMDASH_ORIGIN`)**: Highest priority; ideal for CI/CD pipelines and one-off overrides.
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).
234
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.
235
238
  3. **EmDash CLI Credentials (`~/.config/emdash/auth.json`)**: Fallback when no token is configured in the profile.
236
239
  4. **Emit Custom Credentials (`~/.config/emit/credentials.json`)**: Final fallback.
@@ -238,6 +241,19 @@ Emit resolves authentication credentials using a strict 4-tier priority chain (`
238
241
  **Automatic OAuth Token Refresh on HTTP 401 (`src/client/http.ts`)**:
239
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.
240
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
+
241
257
  ### 5. Workspace Configuration (`.emit/config.json`)
242
258
 
243
259
  Running `emit init` generates `.emit/config.json` with the following keys (`src/utils/config.ts`):
@@ -255,10 +271,10 @@ Running `emit init` generates `.emit/config.json` with the following keys (`src/
255
271
 
256
272
  | Key | Default | Description |
257
273
  | --- | --- | --- |
258
- | `origin` | `"https://example.com"` | Base URL of the target EmDash instance (overridden by `EMDASH_ORIGIN` or active profile) |
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 |
259
275
  | `defaultCollection` | `"posts"` | Default content collection when `--collection` is omitted |
260
276
  | `defaultLocale` | `"ja"` | Default locale used when scaffolding new drafts via `emit new` / `sync_new` |
261
- | `postsSyncLimit` | `100` | Sliding window size for batch article pulls (also supports legacy `syncLimit` fallback) |
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) |
262
278
  | `mediaSyncLimit` | `300` | Default maximum items fetched by `emit pull-media` / `sync_pull_media` |
263
279
  | `concurrency` | `8` | Parallel HTTP fetch concurrency during batch pulls |
264
280
 
@@ -320,8 +336,10 @@ your-blog/
320
336
  └── posts/ # Local Markdown Articles (<yyyyMMdd>_<ulid>-<title>.md)
321
337
  ā”œā”€ā”€ draft/ # Draft articles (named after title; falls back to slug if empty)
322
338
  │ └── 20261104_01M6...-My-New-Post.md
323
- └── published/ # Live published articles
324
- └── 20261015_01M5...-Getting-Started.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
325
343
 
326
344
  ```
327
345
 
@@ -333,7 +351,7 @@ Emit maintains a local SQLite database via Node.js built-in `node:sqlite` (`src/
333
351
  | --- | --- |
334
352
  | `entries` | Caches the latest remote JSON envelope and revision (`rev`) for each synced article |
335
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) |
336
- | `sync_state` | Tracks each article's workspace-relative file path and SHA-256 content hash to detect local modifications (`synced` vs `modified`) |
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>")`) |
337
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 |
338
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 |
339
357
 
@@ -487,9 +505,9 @@ Whether you are a human developer or an autonomous AI agent (Antigravity, Claude
487
505
  * To push a draft: `emit push posts/draft/<filename>.md`
488
506
  * To publish live: update `status: published` and run `emit push posts/draft/<filename>.md --allow-publish`.
489
507
 
490
- ### Preflight Safety Guardrails (G1–G11)
508
+ ### Preflight Safety Guardrails (G1–G12)
491
509
 
492
- To ensure autonomous AI agents and automated scripts cannot corrupt workspace state or accidentally publish unintended content, Emit enforces 11 strict preflight guardrails before executing any write operation:
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:
493
511
 
494
512
  | Guard | Code Name | Description | Severity |
495
513
  | --- | --- | --- | --- |
@@ -504,6 +522,7 @@ To ensure autonomous AI agents and automated scripts cannot corrupt workspace st
504
522
  | **G9** | `MODIFIED_OPAQUE_BLOCK` | Verifies that `<!--ec:block ... -->` opaque fences have not been tampered with or corrupted | Error |
505
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 |
506
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 |
507
526
 
508
527
  ---
509
528
 
@@ -607,13 +626,13 @@ emit pull [id] [options]
607
626
  ```
608
627
 
609
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.
610
- * `-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).
611
630
  * `-c, --collection <collection>`: Collection name (default: `posts`).
612
631
  * `--no-prune`: Do not prune local articles that have fallen outside the sliding window (escape hatch; not intended for routine use).
613
632
  * `--force`: Force overwrite local files and bypass differential hash checking.
614
633
  * `--concurrency <n>`: Parallel processing task limit (default: `8`).
615
634
  * `--posts-only`: Skip automatic remote taxonomy sync. Pulls articles only (local taxonomy candidates hint is still displayed from cached definitions).
616
- * `--media`: Also pull media assets and companion metadata into `media/`.
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.
617
636
  * `--metadata-only`: With `--media`: download only metadata YAML, skip binary file downloads.
618
637
 
619
638
  > [!NOTE]
@@ -621,7 +640,10 @@ emit pull [id] [options]
621
640
 
622
641
  > [!NOTE]
623
642
  > **Why `--no-prune` is rarely needed:**
624
- > 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.
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).
625
647
 
626
648
  **Example:**
627
649
 
@@ -656,11 +678,11 @@ Available Categories:
656
678
  Available Tags:
657
679
  - ai, cloudflare, emit, typescript
658
680
 
659
- Fetching articles from remote (posts, status: all, window: 100, concurrency: 8)...
660
- [1/100] āœ… Pulled hello-world -> posts/published/20261101_01M5...-Hello-World.md
661
- [2/100] āœ… Pulled draft-post -> posts/draft/20261104_01M6...-Draft-Post.md
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
662
684
 
663
- šŸŽ‰ Articles: 100 total (12 updated, 88 skipped/up-to-date)
685
+ šŸŽ‰ Articles: 169 total [100 published, 69 draft] (12 updated, 157 skipped/up-to-date)
664
686
 
665
687
  ```
666
688
 
@@ -709,13 +731,17 @@ emit status [-c, --collection <collection>] [-p, --profile <name>]
709
731
  🟔 [modified] draft posts/draft/20261104_01M6BBB...-AI-Workflow-Guide.md (AI Workflow Guide)
710
732
  ⚪ [untracked] draft posts/draft/20261104_01M6AAA...-Draft-Notes.md (Draft Notes)
711
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
712
735
 
713
- Summary: 3 total (1 synced, 1 modified, 1 untracked)
736
+ Summary: 4 total (1 synced, 1 modified, 1 untracked, 1 invalid)
714
737
  ```
715
738
 
716
739
  > [!NOTE]
717
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]`.
718
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
+
719
745
  ---
720
746
 
721
747
  ### `emit diff`
@@ -735,7 +761,7 @@ emit diff [filePath] [-i, --id <id>] [-p, --profile <name>]
735
761
 
736
762
  ### `emit pull-taxonomies`
737
763
 
738
- 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`.
739
765
 
740
766
  ```bash
741
767
  emit pull-taxonomies
@@ -851,16 +877,19 @@ Emit features a first-class **Model Context Protocol (MCP)** server over stdio.
851
877
 
852
878
  | Tool | Purpose | Key Parameters |
853
879
  | --- | --- | --- |
854
- | `sync_status` | Inspect local sync state (articles returned newest-first) | `collection`, `profile` |
855
- | `sync_pull` | Pull remote articles **and taxonomies** into local workspace (includes taxonomy hints) | `collection`, `id`, `filePath`, `status`, `force`, `noPrune`, `prune`, `postsOnly`, `includeMedia`, `metadataOnly`, `profile` |
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` |
856
882
  | `sync_new` | Scaffold new article with canonical ULID, frontmatter, default byline, and taxonomy hints | `slug` (required), `title`, `collection`, `locale`, `byline`, `profile` |
857
- | `sync_push` | Push local article changes to remote with preflight checks and `media/drafts/*` auto-upload | `filePath` (required), `allowPublish`, `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` |
858
884
  | `sync_diff` | Inspect diff between working file and baseline snapshot | `filePath`, `id`, `profile` |
859
885
  | `sync_pull_taxonomies` | Pull remote category and tag definitions (standalone; returns taxonomy hints) | `profile` |
860
886
  | `sync_pull_media` | Pull remote media files and metadata into `media/` | `limit`, `all`, `downloadFiles`, `profile` |
861
887
  | `profile_list` | List all configured connection profiles | (none) |
862
888
  | `profile_switch` | Switch active profile for multi-site environments | `name` (required), `persist` |
863
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.
892
+
864
893
  ### Autonomous Agent Workflows
865
894
 
866
895
  With Emit connected via MCP, you can give high-level natural language instructions to your AI:
@@ -924,6 +953,12 @@ Push your changes with 'emit push <file>' or discard them before pulling.
924
953
 
925
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.
926
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
+
927
962
  ---
928
963
 
929
964
  ## 🧪 Testing & Quality Assurance
@@ -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;AASzC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuF9D"}
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"}
@@ -21,6 +21,17 @@
21
21
  */
22
22
  import { loadProfilesFile, setProfile, setActiveProfile, removeProfile, resolveTargetProfile, } from '../../utils/profile.js';
23
23
  export function registerProfileCommands(program) {
24
+ /**
25
+ * Profile management is local-file work, and its failures (unknown profile name, ambiguous
26
+ * multi-profile resolution) are user-correctable. Report them as one actionable line with a
27
+ * non-zero exit instead of leaking a raw Node stack trace, matching how pull/push already
28
+ * format errors.
29
+ */
30
+ const reportProfileFailure = (error) => {
31
+ const message = error instanceof Error ? error.message : String(error);
32
+ console.error(`āŒ ${message}`);
33
+ process.exit(1);
34
+ };
24
35
  const profileCmd = program
25
36
  .command('profile')
26
37
  .description('Manage EmDash site connection profiles (multi-site support)');
@@ -58,31 +69,67 @@ export function registerProfileCommands(program) {
58
69
  .option('--workspace <path>', 'Default local workspace directory for this site')
59
70
  .option('--active', 'Set this profile as active immediately')
60
71
  .action((name, options) => {
61
- setProfile(name, {
62
- origin: options.origin,
63
- token: options.token,
64
- workspace: options.workspace,
65
- });
66
- if (options.active) {
67
- setActiveProfile(name);
72
+ try {
73
+ /**
74
+ * `add` doubles as "update", so unspecified options must fall back to what is already
75
+ * stored. Re-running `emit profile add <name> --origin <new>` used to rebuild the entry
76
+ * from scratch, silently wiping a stored token and the workspace link that `emit init`
77
+ * created - the next command then failed with "No authentication found" and directory
78
+ * based profile resolution broke.
79
+ */
80
+ const existing = loadProfilesFile().profiles[name];
81
+ const token = options.token ?? existing?.token ?? '';
82
+ const workspace = options.workspace ?? existing?.workspace;
83
+ const profileConfig = {
84
+ origin: options.origin,
85
+ token,
86
+ ...(workspace ? { workspace } : {}),
87
+ ...(existing?.defaultCollection ? { defaultCollection: existing.defaultCollection } : {}),
88
+ ...(existing?.defaultLocale ? { defaultLocale: existing.defaultLocale } : {}),
89
+ };
90
+ setProfile(name, profileConfig);
91
+ if (options.active) {
92
+ setActiveProfile(name);
93
+ }
94
+ const kept = [];
95
+ if (!options.token && existing?.token)
96
+ kept.push('token');
97
+ if (!options.workspace && workspace)
98
+ kept.push('workspace link');
99
+ console.log(`āœ… Profile "${name}" ${existing ? 'updated' : 'configured'} successfully.` +
100
+ (kept.length
101
+ ? ` Kept the existing ${kept.join(' and ')}; pass --token/--workspace to replace them.`
102
+ : ''));
103
+ }
104
+ catch (error) {
105
+ reportProfileFailure(error);
68
106
  }
69
- console.log(`āœ… Profile "${name}" configured successfully.`);
70
107
  });
71
108
  // emit profile use <name>
72
109
  profileCmd
73
110
  .command('use <name>')
74
111
  .description('Set active default profile')
75
112
  .action((name) => {
76
- setActiveProfile(name);
77
- console.log(`āœ… Active profile set to "${name}".`);
113
+ try {
114
+ setActiveProfile(name);
115
+ console.log(`āœ… Active profile set to "${name}".`);
116
+ }
117
+ catch (error) {
118
+ reportProfileFailure(error);
119
+ }
78
120
  });
79
121
  // emit profile remove <name>
80
122
  profileCmd
81
123
  .command('remove <name>')
82
124
  .description('Remove a profile')
83
125
  .action((name) => {
84
- removeProfile(name);
85
- console.log(`āœ… Profile "${name}" removed.`);
126
+ try {
127
+ removeProfile(name);
128
+ console.log(`āœ… Profile "${name}" removed.`);
129
+ }
130
+ catch (error) {
131
+ reportProfileFailure(error);
132
+ }
86
133
  });
87
134
  // emit profile current
88
135
  profileCmd
@@ -90,7 +137,14 @@ export function registerProfileCommands(program) {
90
137
  .description('Show currently resolved profile and origin')
91
138
  .option('-p, --profile <name>', 'Explicit profile name to inspect')
92
139
  .action((options) => {
93
- const resolved = resolveTargetProfile({ profile: options.profile });
140
+ let resolved;
141
+ try {
142
+ resolved = resolveTargetProfile({ profile: options.profile });
143
+ }
144
+ catch (error) {
145
+ reportProfileFailure(error);
146
+ return;
147
+ }
94
148
  if (!resolved) {
95
149
  console.log('No profile resolved (relying on environment variables or defaults).');
96
150
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"profile.js","sourceRoot":"","sources":["../../../src/cli/commands/profile.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"profile.js","sourceRoot":"","sources":["../../../src/cli/commands/profile.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,9 @@
1
1
  import type { Command } from 'commander';
2
2
  import type { CliContextResolver } from '../context.js';
3
+ export declare function formatPullWindowLabel(status: string | undefined, limit: number): string;
4
+ export declare function formatPullStatusBreakdown(status: string | undefined, items: Array<{
5
+ deleted?: boolean;
6
+ status?: string;
7
+ }>): string;
3
8
  export declare function registerPullCommands(program: Command, resolveContext: CliContextResolver): void;
4
9
  //# sourceMappingURL=pull.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/pull.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASzC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,kBAAkB,GAAG,IAAI,CAyO/F"}
1
+ {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/pull.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUzC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvF;AAED,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,KAAK,EAAE,KAAK,CAAC;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GACnD,MAAM,CAKR;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,kBAAkB,GAAG,IAAI,CA4R/F"}
@@ -19,10 +19,21 @@
19
19
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
20
  * SOFTWARE.
21
21
  */
22
- import { pullArticle } from '../../sync/pull.js';
22
+ import { pullArticle, formatQuarantineNotice } from '../../sync/pull.js';
23
23
  import { pullBatch, SyncPruneConflictError } from '../../sync/orchestrator.js';
24
24
  import { pullTaxonomies, getLocalTaxonomiesSummary, formatTaxonomiesHint, } from '../../sync/taxonomies.js';
25
25
  import { pullMedia } from '../../sync/media.js';
26
+ import { ApiError } from '../../client/error.js';
27
+ export function formatPullWindowLabel(status, limit) {
28
+ return status === 'all' ? `${limit}/status` : `${limit}`;
29
+ }
30
+ export function formatPullStatusBreakdown(status, items) {
31
+ if (status !== 'all')
32
+ return '';
33
+ const publishedCount = items.filter((i) => !i.deleted && i.status === 'published').length;
34
+ const draftCount = items.filter((i) => !i.deleted && i.status === 'draft').length;
35
+ return ` [${publishedCount} published, ${draftCount} draft]`;
36
+ }
26
37
  export function registerPullCommands(program, resolveContext) {
27
38
  // emit pull [id]
28
39
  program
@@ -80,7 +91,8 @@ export function registerPullCommands(program, resolveContext) {
80
91
  const concurrency = options.concurrency
81
92
  ? parseInt(options.concurrency, 10)
82
93
  : ctx.config.concurrency || 8;
83
- console.log(`Fetching articles from remote (${collection}, status: ${options.status}, window: ${limit}, concurrency: ${concurrency})...`);
94
+ const windowLabel = formatPullWindowLabel(options.status, limit);
95
+ console.log(`Fetching articles from remote (${collection}, status: ${options.status}, window: ${windowLabel}, concurrency: ${concurrency})...`);
84
96
  const batchResult = await pullBatch({
85
97
  collection,
86
98
  workspaceRoot: ctx.root,
@@ -97,6 +109,9 @@ export function registerPullCommands(program, resolveContext) {
97
109
  if (item.deleted) {
98
110
  console.log(` [${current}/${total}] šŸ—‘ļø Pruned ${item.slug} -> ${item.filePath}`);
99
111
  }
112
+ else if (item.localEditsPreserved) {
113
+ console.log(`\n [${current}/${total}] šŸ›”ļø Kept local edits for ${item.slug} (${item.filePath})`);
114
+ }
100
115
  else if (!item.skipped) {
101
116
  console.log(` [${current}/${total}] āœ… Pulled ${item.slug} -> ${item.filePath}`);
102
117
  }
@@ -110,7 +125,17 @@ export function registerPullCommands(program, resolveContext) {
110
125
  return;
111
126
  }
112
127
  const deletedMsg = batchResult.deleted ? `, ${batchResult.deleted} pruned` : '';
113
- console.log(`\nšŸŽ‰ Articles: ${batchResult.total} total (${batchResult.updated} updated, ${batchResult.skipped} skipped/up-to-date${deletedMsg})`);
128
+ const statusBreakdown = formatPullStatusBreakdown(options.status, batchResult.items);
129
+ console.log(`\nšŸŽ‰ Articles: ${batchResult.total} total${statusBreakdown} (${batchResult.updated} updated, ${batchResult.skipped} skipped/up-to-date${deletedMsg})`);
130
+ if (batchResult.preserved && batchResult.preserved > 0) {
131
+ console.log(`šŸ›”ļø ${batchResult.preserved} article(s) had unpushed local edits: Emit kept your local version (remote changes were NOT applied). ` +
132
+ `Push them with "emit push <filePath>", or re-run with --force to accept the remote version.`);
133
+ }
134
+ for (const item of batchResult.items) {
135
+ const notice = formatQuarantineNotice(item.quarantined);
136
+ if (notice)
137
+ console.log(`\n${notice}`);
138
+ }
114
139
  // 3. Pull media (if --media)
115
140
  if (includeMedia) {
116
141
  const mediaLimit = ctx.config.mediaSyncLimit || 300;
@@ -140,12 +165,19 @@ export function registerPullCommands(program, resolveContext) {
140
165
  if (res.deleted) {
141
166
  console.log(`šŸ—‘ļø Remote article was deleted. Pruned local file: ${res.filePath}`);
142
167
  }
168
+ else if (res.localEditsPreserved) {
169
+ console.log(`šŸ›”ļø Kept your local edits for ${res.slug} (${res.filePath}); remote changes were NOT applied.`);
170
+ console.log(` Push them with "emit push ${res.filePath}", or re-run with --force to accept the remote version.`);
171
+ }
143
172
  else if (res.skipped) {
144
173
  console.log(`ā„¹ļø Article ${res.slug} is already up-to-date (${res.filePath})`);
145
174
  }
146
175
  else {
147
176
  console.log(`āœ… Pulled ${res.slug} -> ${res.filePath}`);
148
177
  }
178
+ const quarantineNotice = formatQuarantineNotice(res.quarantined);
179
+ if (quarantineNotice)
180
+ console.log(`\n${quarantineNotice}`);
149
181
  }
150
182
  }
151
183
  catch (error) {
@@ -153,6 +185,10 @@ export function registerPullCommands(program, resolveContext) {
153
185
  console.error(`\nāŒ ${error.message}`);
154
186
  process.exit(1);
155
187
  }
188
+ if (error instanceof ApiError) {
189
+ console.error(`\n${error.toActionableString()}`);
190
+ process.exit(1);
191
+ }
156
192
  throw error;
157
193
  }
158
194
  });
@@ -163,30 +199,42 @@ export function registerPullCommands(program, resolveContext) {
163
199
  .option('-p, --profile <name>', 'Explicitly use a specific connection profile')
164
200
  .action(async (options) => {
165
201
  const ctx = resolveContext(options.profile);
166
- console.log('Pulling categories and tags from remote...');
167
- const res = await pullTaxonomies({
168
- workspaceRoot: ctx.root,
169
- origin: ctx.origin,
170
- token: ctx.auth.token,
171
- refreshToken: ctx.auth.refreshToken,
172
- });
173
- const prunedCatMsg = res.deletedCategoriesCount
174
- ? `, ${res.deletedCategoriesCount} categories pruned`
175
- : '';
176
- const prunedTagMsg = res.deletedTagsCount ? `, ${res.deletedTagsCount} tags pruned` : '';
177
- if (res.categoriesFailed) {
178
- console.warn('āš ļø Failed to fetch remote categories: preserved existing local definitions.');
179
- }
180
- if (res.tagsFailed) {
181
- console.warn('āš ļø Failed to fetch remote tags: preserved existing local definitions.');
182
- }
183
- if (!res.categoriesFailed || !res.tagsFailed) {
184
- console.log(`āœ… Successfully synced taxonomies: ${res.categoriesCount} categories, ${res.tagsCount} tags${prunedCatMsg}${prunedTagMsg}.`);
202
+ try {
203
+ console.log('Pulling categories and tags from remote...');
204
+ const res = await pullTaxonomies({
205
+ workspaceRoot: ctx.root,
206
+ origin: ctx.origin,
207
+ token: ctx.auth.token,
208
+ refreshToken: ctx.auth.refreshToken,
209
+ });
210
+ const prunedCatMsg = res.deletedCategoriesCount
211
+ ? `, ${res.deletedCategoriesCount} categories pruned`
212
+ : '';
213
+ const prunedTagMsg = res.deletedTagsCount ? `, ${res.deletedTagsCount} tags pruned` : '';
214
+ if (res.categoriesFailed) {
215
+ console.warn('āš ļø Failed to fetch remote categories: preserved existing local definitions.');
216
+ }
217
+ if (res.tagsFailed) {
218
+ console.warn('āš ļø Failed to fetch remote tags: preserved existing local definitions.');
219
+ }
220
+ if (!res.categoriesFailed || !res.tagsFailed) {
221
+ console.log(`āœ… Successfully synced taxonomies: ${res.categoriesCount} categories, ${res.tagsCount} tags${prunedCatMsg}${prunedTagMsg}.`);
222
+ }
223
+ const taxSummary = getLocalTaxonomiesSummary(ctx.root);
224
+ const taxHint = formatTaxonomiesHint(taxSummary, { maxTags: 60 });
225
+ if (taxHint) {
226
+ console.log(`\n${taxHint}`);
227
+ }
228
+ if (res.categoriesFailed && res.tagsFailed) {
229
+ process.exit(1);
230
+ }
185
231
  }
186
- const taxSummary = getLocalTaxonomiesSummary(ctx.root);
187
- const taxHint = formatTaxonomiesHint(taxSummary, { maxTags: 60 });
188
- if (taxHint) {
189
- console.log(`\n${taxHint}`);
232
+ catch (error) {
233
+ if (error instanceof ApiError) {
234
+ console.error(`\n${error.toActionableString()}`);
235
+ process.exit(1);
236
+ }
237
+ throw error;
190
238
  }
191
239
  });
192
240
  // emit pull-media
@@ -205,19 +253,28 @@ export function registerPullCommands(program, resolveContext) {
205
253
  : options.limit
206
254
  ? parseInt(options.limit, 10)
207
255
  : ctx.config.mediaSyncLimit || 300;
208
- console.log(`Pulling media from remote (${options.all ? 'all' : `limit: ${limit}`})...`);
209
- const res = await pullMedia({
210
- workspaceRoot: ctx.root,
211
- origin: ctx.origin,
212
- token: ctx.auth.token,
213
- refreshToken: ctx.auth.refreshToken,
214
- limit,
215
- all: options.all,
216
- prune: options.prune,
217
- downloadFiles: !options.metadataOnly,
218
- });
219
- const deletedMsg = res.deleted ? `, ${res.deleted} pruned` : '';
220
- console.log(`āœ… Media sync complete: ${res.total} total items (${res.downloaded} downloaded, ${res.skipped} skipped/up-to-date${deletedMsg}).`);
256
+ try {
257
+ console.log(`Pulling media from remote (${options.all ? 'all' : `limit: ${limit}`})...`);
258
+ const res = await pullMedia({
259
+ workspaceRoot: ctx.root,
260
+ origin: ctx.origin,
261
+ token: ctx.auth.token,
262
+ refreshToken: ctx.auth.refreshToken,
263
+ limit,
264
+ all: options.all,
265
+ prune: options.prune,
266
+ downloadFiles: !options.metadataOnly,
267
+ });
268
+ const deletedMsg = res.deleted ? `, ${res.deleted} pruned` : '';
269
+ console.log(`āœ… Media sync complete: ${res.total} total items (${res.downloaded} downloaded, ${res.skipped} skipped/up-to-date${deletedMsg}).`);
270
+ }
271
+ catch (error) {
272
+ if (error instanceof ApiError) {
273
+ console.error(`\n${error.toActionableString()}`);
274
+ process.exit(1);
275
+ }
276
+ throw error;
277
+ }
221
278
  });
222
279
  }
223
280
  //# sourceMappingURL=pull.js.map