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.
- package/README.md +58 -23
- 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 +16 -1
- 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 +69 -3
- 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/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 +52 -13
- 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 +20 -7
- 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 +35 -22
- 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 +101 -72
- 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/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/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 +30 -3
- 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 +13 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +49 -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 +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ā
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
324
|
-
|
|
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ā
|
|
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
|
|
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
|
|
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/
|
|
661
|
-
[2/
|
|
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:
|
|
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:
|
|
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
|
|
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;
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|