emdash-emit 0.1.13 → 0.1.15

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 (146) hide show
  1. package/CHANGELOG.md +108 -1
  2. package/README.md +235 -187
  3. package/dist/cli/commands/delete.d.ts.map +1 -1
  4. package/dist/cli/commands/delete.js +2 -0
  5. package/dist/cli/commands/delete.js.map +1 -1
  6. package/dist/cli/commands/doctor.d.ts.map +1 -1
  7. package/dist/cli/commands/doctor.js +9 -0
  8. package/dist/cli/commands/doctor.js.map +1 -1
  9. package/dist/cli/commands/pull.d.ts +32 -0
  10. package/dist/cli/commands/pull.d.ts.map +1 -1
  11. package/dist/cli/commands/pull.js +108 -9
  12. package/dist/cli/commands/pull.js.map +1 -1
  13. package/dist/cli/commands/push.d.ts.map +1 -1
  14. package/dist/cli/commands/push.js +18 -0
  15. package/dist/cli/commands/push.js.map +1 -1
  16. package/dist/cli/commands/status.d.ts.map +1 -1
  17. package/dist/cli/commands/status.js +15 -5
  18. package/dist/cli/commands/status.js.map +1 -1
  19. package/dist/client/media.d.ts.map +1 -1
  20. package/dist/client/media.js +16 -5
  21. package/dist/client/media.js.map +1 -1
  22. package/dist/db/cache.d.ts +24 -5
  23. package/dist/db/cache.d.ts.map +1 -1
  24. package/dist/db/cache.js +308 -61
  25. package/dist/db/cache.js.map +1 -1
  26. package/dist/mcp/server.d.ts.map +1 -1
  27. package/dist/mcp/server.js +5 -3
  28. package/dist/mcp/server.js.map +1 -1
  29. package/dist/mcp/tools/sync-delete.d.ts.map +1 -1
  30. package/dist/mcp/tools/sync-delete.js +5 -0
  31. package/dist/mcp/tools/sync-delete.js.map +1 -1
  32. package/dist/mcp/tools/sync-doctor.d.ts.map +1 -1
  33. package/dist/mcp/tools/sync-doctor.js +12 -0
  34. package/dist/mcp/tools/sync-doctor.js.map +1 -1
  35. package/dist/mcp/tools/sync-pull.d.ts.map +1 -1
  36. package/dist/mcp/tools/sync-pull.js +49 -12
  37. package/dist/mcp/tools/sync-pull.js.map +1 -1
  38. package/dist/mcp/tools/sync-push.d.ts.map +1 -1
  39. package/dist/mcp/tools/sync-push.js +5 -0
  40. package/dist/mcp/tools/sync-push.js.map +1 -1
  41. package/dist/mcp/tools/sync-status.d.ts.map +1 -1
  42. package/dist/mcp/tools/sync-status.js +6 -2
  43. package/dist/mcp/tools/sync-status.js.map +1 -1
  44. package/dist/sync/conflicts.d.ts.map +1 -1
  45. package/dist/sync/conflicts.js +44 -0
  46. package/dist/sync/conflicts.js.map +1 -1
  47. package/dist/sync/delete.d.ts +3 -1
  48. package/dist/sync/delete.d.ts.map +1 -1
  49. package/dist/sync/delete.js +7 -20
  50. package/dist/sync/delete.js.map +1 -1
  51. package/dist/sync/diff.d.ts.map +1 -1
  52. package/dist/sync/diff.js +18 -2
  53. package/dist/sync/diff.js.map +1 -1
  54. package/dist/sync/doctor.d.ts +28 -1
  55. package/dist/sync/doctor.d.ts.map +1 -1
  56. package/dist/sync/doctor.js +393 -45
  57. package/dist/sync/doctor.js.map +1 -1
  58. package/dist/sync/guards.d.ts +45 -0
  59. package/dist/sync/guards.d.ts.map +1 -0
  60. package/dist/sync/guards.js +235 -0
  61. package/dist/sync/guards.js.map +1 -0
  62. package/dist/sync/media/body_scan.d.ts +15 -0
  63. package/dist/sync/media/body_scan.d.ts.map +1 -0
  64. package/dist/sync/media/body_scan.js +132 -0
  65. package/dist/sync/media/body_scan.js.map +1 -0
  66. package/dist/sync/media/companions.d.ts +20 -0
  67. package/dist/sync/media/companions.d.ts.map +1 -0
  68. package/dist/sync/media/companions.js +111 -0
  69. package/dist/sync/media/companions.js.map +1 -0
  70. package/dist/sync/media/index.d.ts +9 -0
  71. package/dist/sync/media/index.d.ts.map +1 -0
  72. package/dist/sync/media/index.js +9 -0
  73. package/dist/sync/media/index.js.map +1 -0
  74. package/dist/sync/media/mirror_item.d.ts +27 -0
  75. package/dist/sync/media/mirror_item.d.ts.map +1 -0
  76. package/dist/sync/media/mirror_item.js +207 -0
  77. package/dist/sync/media/mirror_item.js.map +1 -0
  78. package/dist/sync/media/naming_mirror.d.ts +36 -0
  79. package/dist/sync/media/naming_mirror.d.ts.map +1 -0
  80. package/dist/sync/media/naming_mirror.js +143 -0
  81. package/dist/sync/media/naming_mirror.js.map +1 -0
  82. package/dist/sync/media/pull_article.d.ts +37 -0
  83. package/dist/sync/media/pull_article.d.ts.map +1 -0
  84. package/dist/sync/media/pull_article.js +273 -0
  85. package/dist/sync/media/pull_article.js.map +1 -0
  86. package/dist/sync/media/pull_library.d.ts +12 -0
  87. package/dist/sync/media/pull_library.d.ts.map +1 -0
  88. package/dist/sync/media/pull_library.js +207 -0
  89. package/dist/sync/media/pull_library.js.map +1 -0
  90. package/dist/sync/media/remote_index.d.ts +18 -0
  91. package/dist/sync/media/remote_index.d.ts.map +1 -0
  92. package/dist/sync/media/remote_index.js +94 -0
  93. package/dist/sync/media/remote_index.js.map +1 -0
  94. package/dist/sync/media/types.d.ts +191 -0
  95. package/dist/sync/media/types.d.ts.map +1 -0
  96. package/dist/sync/media/types.js +11 -0
  97. package/dist/sync/media/types.js.map +1 -0
  98. package/dist/sync/media.d.ts +1 -74
  99. package/dist/sync/media.d.ts.map +1 -1
  100. package/dist/sync/media.js +1 -592
  101. package/dist/sync/media.js.map +1 -1
  102. package/dist/sync/merge.d.ts +11 -0
  103. package/dist/sync/merge.d.ts.map +1 -1
  104. package/dist/sync/merge.js +53 -6
  105. package/dist/sync/merge.js.map +1 -1
  106. package/dist/sync/orchestrator.d.ts +23 -0
  107. package/dist/sync/orchestrator.d.ts.map +1 -1
  108. package/dist/sync/orchestrator.js +85 -1
  109. package/dist/sync/orchestrator.js.map +1 -1
  110. package/dist/sync/preflight.d.ts +1 -1
  111. package/dist/sync/preflight.d.ts.map +1 -1
  112. package/dist/sync/preflight.js +127 -15
  113. package/dist/sync/preflight.js.map +1 -1
  114. package/dist/sync/pull.d.ts +21 -0
  115. package/dist/sync/pull.d.ts.map +1 -1
  116. package/dist/sync/pull.js +278 -17
  117. package/dist/sync/pull.js.map +1 -1
  118. package/dist/sync/push.d.ts +7 -0
  119. package/dist/sync/push.d.ts.map +1 -1
  120. package/dist/sync/push.js +135 -58
  121. package/dist/sync/push.js.map +1 -1
  122. package/dist/sync/status.d.ts +4 -0
  123. package/dist/sync/status.d.ts.map +1 -1
  124. package/dist/sync/status.js +68 -4
  125. package/dist/sync/status.js.map +1 -1
  126. package/dist/utils/config.d.ts +25 -0
  127. package/dist/utils/config.d.ts.map +1 -1
  128. package/dist/utils/config.js +28 -2
  129. package/dist/utils/config.js.map +1 -1
  130. package/dist/utils/frontmatter.d.ts +1 -1
  131. package/dist/utils/frontmatter.d.ts.map +1 -1
  132. package/dist/utils/frontmatter.js +15 -4
  133. package/dist/utils/frontmatter.js.map +1 -1
  134. package/dist/utils/media-body-refs.d.ts +81 -0
  135. package/dist/utils/media-body-refs.d.ts.map +1 -0
  136. package/dist/utils/media-body-refs.js +240 -0
  137. package/dist/utils/media-body-refs.js.map +1 -0
  138. package/dist/utils/media-refs.d.ts +44 -0
  139. package/dist/utils/media-refs.d.ts.map +1 -0
  140. package/dist/utils/media-refs.js +101 -0
  141. package/dist/utils/media-refs.js.map +1 -0
  142. package/dist/utils/naming.d.ts +31 -0
  143. package/dist/utils/naming.d.ts.map +1 -1
  144. package/dist/utils/naming.js +58 -0
  145. package/dist/utils/naming.js.map +1 -1
  146. package/package.json +1 -1
@@ -0,0 +1,273 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { unwrapEnvelope } from '../../client/api.js';
4
+ import { ApiError } from '../../client/error.js';
5
+ import { createAuthedFetch } from '../../client/http.js';
6
+ import { extractMediaUlid } from '../../client/mapper.js';
7
+ import { parseMarkdown } from '../../utils/frontmatter.js';
8
+ import { collectArticleReferencedMediaIds, collectArticleStorageKeys } from './body_scan.js';
9
+ import { collectLocalMediaMirrorCandidates } from './companions.js';
10
+ import { syncSingleMediaItem } from './mirror_item.js';
11
+ /**
12
+ * Pulls media referenced by a specific article (by ULID or file path).
13
+ */
14
+ export async function pullArticleMedia(options) {
15
+ const fetchFn = createAuthedFetch(options);
16
+ const collection = options.collection || 'posts';
17
+ const downloadFiles = options.downloadFiles !== false;
18
+ const mediaDir = path.join(options.workspaceRoot, 'media');
19
+ fs.mkdirSync(mediaDir, { recursive: true });
20
+ let articleId = options.article;
21
+ let articleContent = null;
22
+ // 1. Try resolving as direct file path
23
+ const directPath = path.isAbsolute(options.article)
24
+ ? options.article
25
+ : path.resolve(options.workspaceRoot, options.article);
26
+ if (fs.existsSync(directPath) && fs.statSync(directPath).isFile()) {
27
+ articleContent = fs.readFileSync(directPath, 'utf8');
28
+ try {
29
+ const parsed = parseMarkdown(articleContent);
30
+ articleId = parsed.frontmatter.id || articleId;
31
+ }
32
+ catch {
33
+ // Keep articleId as fallback
34
+ }
35
+ }
36
+ else {
37
+ // 2. Try finding file in draft/published
38
+ for (const status of ['draft', 'published']) {
39
+ const statusDir = path.join(options.workspaceRoot, collection, status);
40
+ if (!fs.existsSync(statusDir))
41
+ continue;
42
+ const found = fs
43
+ .readdirSync(statusDir)
44
+ .find((f) => f.endsWith('.md') && f.includes(options.article));
45
+ if (found) {
46
+ articleContent = fs.readFileSync(path.join(statusDir, found), 'utf8');
47
+ try {
48
+ const parsed = parseMarkdown(articleContent);
49
+ articleId = parsed.frontmatter.id || articleId;
50
+ }
51
+ catch {
52
+ // fallback
53
+ }
54
+ break;
55
+ }
56
+ }
57
+ }
58
+ // 3. If still absent locally, fetch remote article content from EmDash API
59
+ if (!articleContent) {
60
+ const url = new URL(`/_emdash/api/content/${collection}/${options.article}`, options.origin);
61
+ const res = await fetchFn(url.toString(), {
62
+ headers: {
63
+ Authorization: `Bearer ${options.token}`,
64
+ Accept: 'application/json',
65
+ },
66
+ });
67
+ if (!res.ok) {
68
+ throw new ApiError({
69
+ message: `Failed to fetch remote article for media resolution: ${res.statusText}`,
70
+ code: 'ARTICLE_NOT_FOUND',
71
+ status: res.status,
72
+ endpoint: url.toString(),
73
+ suggestion: `Verify that article '${options.article}' exists in collection '${collection}' on remote.`,
74
+ });
75
+ }
76
+ const json = await res.json();
77
+ const data = unwrapEnvelope(json);
78
+ const item = (data.item || data);
79
+ articleId = item.id || options.article;
80
+ // Build a content string from the remote representation so references can be collected even when
81
+ // no local file exists yet.
82
+ const remoteFi = item.featured_image || item.featuredImage;
83
+ const fiId = extractMediaUlid(remoteFi);
84
+ const fiPart = fiId ? `featuredImage: ${fiId}\n` : '';
85
+ const bodyPart = typeof item.content === 'string'
86
+ ? item.content
87
+ : JSON.stringify(item.content || item.body || '');
88
+ articleContent = `---\n${fiPart}---\n${bodyPart}`;
89
+ }
90
+ const mediaIds = collectArticleReferencedMediaIds(articleContent);
91
+ // Body storageKeys are the only handle available when a migrated body bakes the R2 `storageKey`
92
+ // instead of the D1 record id. They must never be fed to `GET /_emdash/api/media/{id}` as if they
93
+ // were ids: that lookup keys on the primary key, so it returns 404 for a record that does exist.
94
+ const storageKeyDescriptors = collectArticleStorageKeys(articleContent);
95
+ const descriptorsByKey = new Map();
96
+ for (const descriptor of storageKeyDescriptors) {
97
+ descriptorsByKey.set(descriptor.storageKey, descriptor);
98
+ }
99
+ const allKeys = Array.from(descriptorsByKey.keys()).sort();
100
+ if (mediaIds.length === 0 && allKeys.length === 0) {
101
+ return {
102
+ total: 0,
103
+ downloaded: 0,
104
+ skipped: 0,
105
+ failed: 0,
106
+ articleId,
107
+ mediaIds: [],
108
+ orphanStorageKeys: [],
109
+ };
110
+ }
111
+ let downloadedCount = 0;
112
+ let skippedCount = 0;
113
+ // A body ULID that no canonical lookup resolves is the normal shape of a migrated corpus: the URL
114
+ // carries an R2 storageKey, not a media id. Only a reference that also survives every storageKey
115
+ // check is a problem, so a failed id lookup alone is never reported as `failed`.
116
+ let unresolvedIdCount = 0;
117
+ const recoveredStorageKeys = new Set();
118
+ // Ids whose canonical lookup succeeded. A body key sharing one of these ULIDs is a confirmed match:
119
+ // the record exists, it simply carries a storageKey that differs from the one baked into the body.
120
+ const resolvedRecordIds = new Set();
121
+ // 1. Canonical path: resolve each referenced media record id and mirror it normally.
122
+ for (const mId of mediaIds) {
123
+ try {
124
+ const mediaUrl = new URL(`/_emdash/api/media/${mId}`, options.origin);
125
+ const res = await fetchFn(mediaUrl.toString(), {
126
+ headers: {
127
+ Authorization: `Bearer ${options.token}`,
128
+ Accept: 'application/json',
129
+ },
130
+ });
131
+ if (!res.ok) {
132
+ unresolvedIdCount++;
133
+ continue;
134
+ }
135
+ const json = await res.json();
136
+ const envelope = unwrapEnvelope(json);
137
+ const item = (envelope.item || envelope);
138
+ const result = await syncSingleMediaItem({
139
+ item,
140
+ mediaDir,
141
+ origin: options.origin,
142
+ token: options.token,
143
+ downloadFiles,
144
+ fetchFn,
145
+ cache: options.cache,
146
+ });
147
+ if (result.downloaded)
148
+ downloadedCount++;
149
+ else if (result.skipped)
150
+ skippedCount++;
151
+ resolvedRecordIds.add(mId.toUpperCase());
152
+ if (item.storageKey)
153
+ recoveredStorageKeys.add(item.storageKey);
154
+ }
155
+ catch {
156
+ unresolvedIdCount++;
157
+ }
158
+ }
159
+ // 2. Unresolved storageKeys: a body key that no canonical id lookup mirrored is NOT proof of a
160
+ // missing record. The record can exist under a different primary key, so Emit re-checks the key
161
+ // against confirmed evidence only — a record it just resolved under the same ULID, or the local
162
+ // mirror metadata (companion YAML `id` + `storageKey`) — and reports what still fails as
163
+ // unresolved. Emit never fetches such an asset and never registers a record for it.
164
+ const mirrorCandidates = collectLocalMediaMirrorCandidates(mediaDir);
165
+ const verifiedStorageKeys = [];
166
+ const orphanStorageKeys = [];
167
+ for (const key of allKeys) {
168
+ if (recoveredStorageKeys.has(key))
169
+ continue;
170
+ const ulids = (key.match(/[0-9A-HJ-NP-Z]{26}/gi) ?? []).map((u) => u.toUpperCase());
171
+ // Only two things count as evidence: a record Emit successfully fetched under this exact ULID, or
172
+ // confirmed local mirror metadata naming it. A body reference alone is never self-confirming — that
173
+ // is precisely the case where the asset must be reported instead of assumed healthy.
174
+ const confirmed = ulids.some((ulid) => resolvedRecordIds.has(ulid) || mirrorCandidates.has(ulid));
175
+ if (confirmed) {
176
+ verifiedStorageKeys.push(key);
177
+ continue;
178
+ }
179
+ orphanStorageKeys.push(key);
180
+ }
181
+ return {
182
+ total: mediaIds.length + verifiedStorageKeys.length + orphanStorageKeys.length,
183
+ downloaded: downloadedCount,
184
+ skipped: skippedCount,
185
+ failed: orphanStorageKeys.length,
186
+ unresolvedIds: unresolvedIdCount,
187
+ articleId,
188
+ mediaIds,
189
+ verifiedStorageKeys,
190
+ orphanStorageKeys,
191
+ };
192
+ }
193
+ /**
194
+ * Mirrors only the assets a batch of pulled articles actually references, reusing the records the lightweight
195
+ * index walk already fetched instead of paying for a per-asset `GET /_emdash/api/media/{id}`.
196
+ *
197
+ * This is the mirror half of the default-ON local-first pull: the walk phase builds `RemoteStorageKeyIndex`
198
+ * (JSON only, no binaries), and this phase downloads the binary + companion YAML for exactly the storageKeys
199
+ * found in article bodies. A key the walk did not report is NOT proof of a missing record — the record can
200
+ * exist under a different primary key while the body carries an R2 storageKey — so it is re-checked against
201
+ * confirmed evidence only (a walked record resolved under that exact ULID, or local mirror metadata) and what
202
+ * still fails is reported as `orphanStorageKeys`. Emit never fetches such an asset and never registers one.
203
+ */
204
+ export async function mirrorReferencedAssetsFromIndex(referencedStorageKeys, index, options) {
205
+ const fetchFn = createAuthedFetch(options);
206
+ const downloadFiles = options.downloadFiles !== false;
207
+ const mediaDir = path.join(options.workspaceRoot, 'media');
208
+ fs.mkdirSync(mediaDir, { recursive: true });
209
+ const mirroredStorageKeys = new Set();
210
+ let downloadedCount = 0;
211
+ let skippedCount = 0;
212
+ // Walk every referenced key once. A key resolves to a walked record either directly (the walk reported
213
+ // that exact storageKey) or through a ULID the walk resolved under that record id — the migrated-corpus
214
+ // case where the body URL bakes an R2 storageKey that differs from the record's own primary key.
215
+ const referenced = [...referencedStorageKeys];
216
+ const mirrorCandidates = collectLocalMediaMirrorCandidates(mediaDir);
217
+ const verifiedStorageKeys = [];
218
+ const orphanStorageKeys = [];
219
+ for (const key of referenced) {
220
+ let item = index.recordsByStorageKey.get(key);
221
+ if (!item) {
222
+ // The body key may itself be a record id the walk stored under `recordsById`, or share a ULID with a
223
+ // walked record. Reuse that record rather than re-fetching it.
224
+ const ulids = (key.match(/[0-9A-HJ-NP-Z]{26}/gi) ?? []).map((u) => u.toUpperCase());
225
+ for (const ulid of ulids) {
226
+ const byId = index.recordsById.get(ulid);
227
+ if (byId) {
228
+ item = byId;
229
+ break;
230
+ }
231
+ }
232
+ }
233
+ if (item) {
234
+ const result = await syncSingleMediaItem({
235
+ item,
236
+ mediaDir,
237
+ origin: options.origin,
238
+ token: options.token,
239
+ downloadFiles,
240
+ fetchFn,
241
+ cache: options.cache,
242
+ });
243
+ if (result.downloaded)
244
+ downloadedCount++;
245
+ else if (result.skipped)
246
+ skippedCount++;
247
+ if (item.storageKey)
248
+ mirroredStorageKeys.add(item.storageKey);
249
+ mirroredStorageKeys.add(key);
250
+ continue;
251
+ }
252
+ // No walked record owns this key. Only two things count as evidence it is healthy: a local mirror
253
+ // companion already names the ULID, or the walk stored a matching storageKey. A bare body reference is
254
+ // never self-confirming — that is precisely the case to report rather than assume healthy.
255
+ const ulids = (key.match(/[0-9A-HJ-NP-Z]{26}/gi) ?? []).map((u) => u.toUpperCase());
256
+ const confirmed = index.storageKeys.has(key) ||
257
+ ulids.some((ulid) => index.recordsById.has(ulid) || mirrorCandidates.has(ulid));
258
+ if (confirmed) {
259
+ verifiedStorageKeys.push(key);
260
+ }
261
+ else {
262
+ orphanStorageKeys.push(key);
263
+ }
264
+ }
265
+ return {
266
+ mirroredStorageKeys,
267
+ downloaded: downloadedCount,
268
+ skipped: skippedCount,
269
+ verifiedStorageKeys,
270
+ orphanStorageKeys,
271
+ };
272
+ }
273
+ //# sourceMappingURL=pull_article.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pull_article.js","sourceRoot":"","sources":["../../../src/sync/media/pull_article.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { type PullMediaOptions, type PullMediaResult } from './types.js';
2
+ /**
3
+ * Pulls media metadata and files from EmDash API into media/ directory with cursor pagination.
4
+ * media/<markdownRef>.yaml
5
+ * media/<markdownRef>.<ext>
6
+ *
7
+ * `markdownRef` is the sanitized storageKey basename (`storageKeyToMarkdownRef`), never the record id, so the
8
+ * mirror on disk shares one stem with the body URL that references it; the original `filename` survives only
9
+ * inside the companion YAML. See `mediaMirrorNames()` for the full naming and collision rule.
10
+ */
11
+ export declare function pullMedia(options: PullMediaOptions): Promise<PullMediaResult>;
12
+ //# sourceMappingURL=pull_library.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pull_library.d.ts","sourceRoot":"","sources":["../../../src/sync/media/pull_library.ts"],"names":[],"mappings":"AAYA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EAErB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;GAQG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAuMnF"}
@@ -0,0 +1,207 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { unwrapEnvelope } from '../../client/api.js';
4
+ import { ApiError } from '../../client/error.js';
5
+ import { createAuthedFetch } from '../../client/http.js';
6
+ import { DEFAULT_CONFIG } from '../../utils/config.js';
7
+ import { collectArticleStorageKeys } from './body_scan.js';
8
+ import { collectLocalMediaMirrorCandidates } from './companions.js';
9
+ import { syncSingleMediaItem } from './mirror_item.js';
10
+ import { mediaMirrorNames, mediaRecordIdForStem } from './naming_mirror.js';
11
+ import { pullArticleMedia } from './pull_article.js';
12
+ import { findUnmirroredStorageKeys } from './remote_index.js';
13
+ import { MEDIA_LIST_PAGE_SIZE, } from './types.js';
14
+ /**
15
+ * Pulls media metadata and files from EmDash API into media/ directory with cursor pagination.
16
+ * media/<markdownRef>.yaml
17
+ * media/<markdownRef>.<ext>
18
+ *
19
+ * `markdownRef` is the sanitized storageKey basename (`storageKeyToMarkdownRef`), never the record id, so the
20
+ * mirror on disk shares one stem with the body URL that references it; the original `filename` survives only
21
+ * inside the companion YAML. See `mediaMirrorNames()` for the full naming and collision rule.
22
+ */
23
+ export async function pullMedia(options) {
24
+ if (options.article) {
25
+ return pullArticleMedia({
26
+ workspaceRoot: options.workspaceRoot,
27
+ origin: options.origin,
28
+ token: options.token,
29
+ refreshToken: options.refreshToken,
30
+ article: options.article,
31
+ collection: options.collection,
32
+ downloadFiles: options.downloadFiles,
33
+ fetchFn: options.fetchFn,
34
+ cache: options.cache,
35
+ });
36
+ }
37
+ const fetchFn = createAuthedFetch(options);
38
+ const targetTotal = options.all ? Infinity : (options.limit ?? DEFAULT_CONFIG.mediaSyncLimit);
39
+ const downloadFiles = options.downloadFiles !== false;
40
+ const batchSize = MEDIA_LIST_PAGE_SIZE;
41
+ const mediaDir = path.join(options.workspaceRoot, 'media');
42
+ fs.mkdirSync(mediaDir, { recursive: true });
43
+ // Every mirrored companion is a confirmed storageKey source, including the ones written before Emit
44
+ // persisted the field, whose `url:` still names the key. Seeding from disk means `--prune` never
45
+ // deletes an asset that this run's media window simply did not reach.
46
+ const activeStorageKeys = new Set();
47
+ for (const candidate of collectLocalMediaMirrorCandidates(mediaDir).values()) {
48
+ if (candidate.storageKey)
49
+ activeStorageKeys.add(candidate.storageKey);
50
+ }
51
+ // Metadata for the coverage report and the storageKey census. `pullMedia` already walks the library
52
+ // cursor-by-cursor, so it owns the index instead of paying for a second one.
53
+ const remoteIndex = {
54
+ storageKeys: activeStorageKeys,
55
+ mediaIdsByStorageKey: new Map(),
56
+ recordsByStorageKey: new Map(),
57
+ recordsById: new Map(),
58
+ totalCount: null,
59
+ complete: false,
60
+ scannedCount: 0,
61
+ };
62
+ const activeMediaBaseNames = new Set();
63
+ // Only records the remote walk actually reports may authorise a mirror to stay. Seeding this from local
64
+ // companions instead would make every existing file self-exempt and prune nothing at all.
65
+ const activeMediaIds = new Set();
66
+ let totalProcessed = 0;
67
+ let downloadedCount = 0;
68
+ let skippedCount = 0;
69
+ let cursor = undefined;
70
+ while (totalProcessed < targetTotal) {
71
+ const remaining = targetTotal - totalProcessed;
72
+ const currentBatch = Math.min(remaining, batchSize);
73
+ const url = new URL(`/_emdash/api/media`, options.origin);
74
+ url.searchParams.set('limit', String(currentBatch));
75
+ if (cursor) {
76
+ url.searchParams.set('cursor', cursor);
77
+ }
78
+ const res = await fetchFn(url.toString(), {
79
+ method: 'GET',
80
+ headers: {
81
+ Authorization: `Bearer ${options.token}`,
82
+ Accept: 'application/json',
83
+ },
84
+ });
85
+ if (!res.ok) {
86
+ throw new ApiError({
87
+ message: `Failed to list media: ${res.statusText}`,
88
+ code: 'MEDIA_LIST_FAILED',
89
+ status: res.status,
90
+ endpoint: url.toString(),
91
+ suggestion: 'Verify network connection and token permissions.',
92
+ });
93
+ }
94
+ const json = await res.json();
95
+ const data = unwrapEnvelope(json);
96
+ if (typeof data.totalCount === 'number')
97
+ remoteIndex.totalCount = data.totalCount;
98
+ const rawItems = (Array.isArray(data.items) ? data.items : Array.isArray(data) ? data : []);
99
+ if (rawItems.length === 0) {
100
+ // An empty page ends the listing regardless of any cursor the server echoes back.
101
+ remoteIndex.complete = true;
102
+ break;
103
+ }
104
+ for (const item of rawItems) {
105
+ const names = mediaMirrorNames(item);
106
+ activeMediaBaseNames.add(names.markdownRef);
107
+ for (const legacy of names.legacyBaseNames) {
108
+ activeMediaBaseNames.add(legacy);
109
+ }
110
+ activeMediaIds.add(item.id);
111
+ if (item.storageKey) {
112
+ activeStorageKeys.add(item.storageKey);
113
+ remoteIndex.mediaIdsByStorageKey.set(item.storageKey, item.id);
114
+ }
115
+ const result = await syncSingleMediaItem({
116
+ item,
117
+ mediaDir,
118
+ origin: options.origin,
119
+ token: options.token,
120
+ downloadFiles,
121
+ fetchFn,
122
+ cache: options.cache,
123
+ });
124
+ if (result.downloaded)
125
+ downloadedCount++;
126
+ else if (result.skipped)
127
+ skippedCount++;
128
+ totalProcessed++;
129
+ remoteIndex.scannedCount++;
130
+ if (totalProcessed >= targetTotal) {
131
+ break;
132
+ }
133
+ }
134
+ if (!data.nextCursor || typeof data.nextCursor !== 'string') {
135
+ remoteIndex.complete = true;
136
+ break;
137
+ }
138
+ cursor = data.nextCursor;
139
+ }
140
+ // Pruning logic for media: if --prune or --all is enabled, delete local media not present on remote
141
+ let deletedCount = 0;
142
+ if ((options.prune || options.all) && fs.existsSync(mediaDir)) {
143
+ const files = fs.readdirSync(mediaDir);
144
+ for (const file of files) {
145
+ // Leftover `legacy_*` mirrors from the removed file-route fallback are never matched by the
146
+ // mediaId pattern below, so they survive pruning and must be deleted by hand.
147
+ if (file.startsWith('legacy_'))
148
+ continue;
149
+ // A mirror is identified with its companion YAML: the stem names the storageKey, and the record id
150
+ // lives inside the companion. Only a file Emit can attribute to a record may be pruned — an
151
+ // unattributable migrated name is kept, because deleting an asset over a guess is unrecoverable.
152
+ const stem = file.replace(/\.[^.]+$/, '');
153
+ if (activeMediaBaseNames.has(stem) || activeMediaBaseNames.has(stem.replace(/^\d{8}_/, ''))) {
154
+ continue;
155
+ }
156
+ const companionId = mediaRecordIdForStem(mediaDir, stem);
157
+ if (companionId && !activeMediaIds.has(companionId)) {
158
+ try {
159
+ fs.unlinkSync(path.join(mediaDir, file));
160
+ deletedCount++;
161
+ }
162
+ catch {
163
+ // Ignore unlink errors
164
+ }
165
+ }
166
+ }
167
+ }
168
+ // A library-wide walk is exhaustive, so it doubles as the storageKey census: every article reference
169
+ // that neither a local companion nor any remote record names is reported. The list endpoint exposes no
170
+ // storageKey filter, which is why the census requires the full walk `--all` already performs.
171
+ let referencedUnmirroredStorageKeys = undefined;
172
+ if (options.all) {
173
+ const referenced = new Set();
174
+ for (const status of ['draft', 'published']) {
175
+ const statusDir = path.join(options.workspaceRoot, options.collection || 'posts', status);
176
+ if (!fs.existsSync(statusDir))
177
+ continue;
178
+ for (const file of fs.readdirSync(statusDir)) {
179
+ if (!file.endsWith('.md'))
180
+ continue;
181
+ try {
182
+ const content = fs.readFileSync(path.join(statusDir, file), 'utf8');
183
+ for (const descriptor of collectArticleStorageKeys(content)) {
184
+ referenced.add(descriptor.storageKey);
185
+ }
186
+ }
187
+ catch {
188
+ // An unreadable article contributes nothing to the census.
189
+ }
190
+ }
191
+ }
192
+ if (referenced.size > 0) {
193
+ referencedUnmirroredStorageKeys = findUnmirroredStorageKeys(referenced, activeStorageKeys, remoteIndex);
194
+ }
195
+ }
196
+ return {
197
+ total: totalProcessed,
198
+ downloaded: downloadedCount,
199
+ skipped: skippedCount,
200
+ deleted: deletedCount,
201
+ remoteTotalCount: remoteIndex.complete
202
+ ? (remoteIndex.totalCount ?? remoteIndex.scannedCount)
203
+ : remoteIndex.totalCount,
204
+ referencedUnmirroredStorageKeys,
205
+ };
206
+ }
207
+ //# sourceMappingURL=pull_library.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pull_library.js","sourceRoot":"","sources":["../../../src/sync/media/pull_library.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { type RemoteStorageKeyIndex } from './types.js';
2
+ export declare function collectRemoteStorageKeys(options: {
3
+ workspaceRoot: string;
4
+ origin: string;
5
+ token: string;
6
+ refreshToken?: string;
7
+ /** Stop after this many records. Defaults to unlimited, which is the whole library. */
8
+ maxItems?: number;
9
+ fetchFn?: typeof fetch;
10
+ }): Promise<RemoteStorageKeyIndex>;
11
+ /**
12
+ * Reports body storageKeys that no local mirror names and that a full remote library walk does not
13
+ * contain. A key is only listed when the walk was complete: an early stop proves nothing about the
14
+ * rest of the library, so an incomplete walk yields nothing. Even a confirmed absence is reported as a
15
+ * lookup result rather than as a missing record — Emit mirrors records and never repairs server data.
16
+ */
17
+ export declare function findUnmirroredStorageKeys(referenced: Iterable<string>, localMirrorKeys: Iterable<string>, remoteIndex: RemoteStorageKeyIndex): string[];
18
+ //# sourceMappingURL=remote_index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote_index.d.ts","sourceRoot":"","sources":["../../../src/sync/media/remote_index.ts"],"names":[],"mappings":"AAGA,OAAO,EAA8C,KAAK,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEpG,wBAAsB,wBAAwB,CAAC,OAAO,EAAE;IACtD,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA6EjC;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,EAC5B,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,EACjC,WAAW,EAAE,qBAAqB,GACjC,MAAM,EAAE,CASV"}
@@ -0,0 +1,94 @@
1
+ import { unwrapEnvelope } from '../../client/api.js';
2
+ import { ApiError } from '../../client/error.js';
3
+ import { createAuthedFetch } from '../../client/http.js';
4
+ import { MEDIA_LIST_PAGE_SIZE } from './types.js';
5
+ export async function collectRemoteStorageKeys(options) {
6
+ const fetchFn = createAuthedFetch(options);
7
+ const batchSize = MEDIA_LIST_PAGE_SIZE;
8
+ const maxItems = options.maxItems ?? Infinity;
9
+ const storageKeys = new Set();
10
+ const mediaIdsByStorageKey = new Map();
11
+ const recordsByStorageKey = new Map();
12
+ const recordsById = new Map();
13
+ let totalCount = null;
14
+ let scanned = 0;
15
+ let complete = false;
16
+ let cursor = undefined;
17
+ for (;;) {
18
+ const remaining = maxItems - scanned;
19
+ if (remaining <= 0)
20
+ break;
21
+ const currentBatch = Math.min(remaining, batchSize);
22
+ const url = new URL('/_emdash/api/media', options.origin);
23
+ url.searchParams.set('limit', String(currentBatch));
24
+ if (cursor)
25
+ url.searchParams.set('cursor', cursor);
26
+ const res = await fetchFn(url.toString(), {
27
+ headers: {
28
+ Authorization: `Bearer ${options.token}`,
29
+ Accept: 'application/json',
30
+ },
31
+ });
32
+ if (!res.ok) {
33
+ throw new ApiError({
34
+ message: `Failed to list media: ${res.statusText}`,
35
+ code: 'MEDIA_LIST_FAILED',
36
+ status: res.status,
37
+ endpoint: url.toString(),
38
+ suggestion: 'Verify network connection and token permissions.',
39
+ });
40
+ }
41
+ const data = unwrapEnvelope(await res.json());
42
+ if (typeof data.totalCount === 'number')
43
+ totalCount = data.totalCount;
44
+ const rawItems = (Array.isArray(data.items) ? data.items : Array.isArray(data.data) ? data.data : []);
45
+ for (const item of rawItems) {
46
+ scanned++;
47
+ if (item.id)
48
+ recordsById.set(item.id.toUpperCase(), item);
49
+ if (typeof item.storageKey === 'string' && item.storageKey) {
50
+ storageKeys.add(item.storageKey);
51
+ // First record wins a repeated key, so reporting a match stays deterministic.
52
+ if (item.id && !mediaIdsByStorageKey.has(item.storageKey)) {
53
+ mediaIdsByStorageKey.set(item.storageKey, item.id);
54
+ }
55
+ if (!recordsByStorageKey.has(item.storageKey)) {
56
+ recordsByStorageKey.set(item.storageKey, item);
57
+ }
58
+ }
59
+ }
60
+ if (rawItems.length === 0 || typeof data.nextCursor !== 'string' || !data.nextCursor) {
61
+ complete = true;
62
+ break;
63
+ }
64
+ cursor = data.nextCursor;
65
+ }
66
+ return {
67
+ storageKeys,
68
+ mediaIdsByStorageKey,
69
+ recordsByStorageKey,
70
+ recordsById,
71
+ totalCount,
72
+ complete,
73
+ scannedCount: scanned,
74
+ };
75
+ }
76
+ /**
77
+ * Reports body storageKeys that no local mirror names and that a full remote library walk does not
78
+ * contain. A key is only listed when the walk was complete: an early stop proves nothing about the
79
+ * rest of the library, so an incomplete walk yields nothing. Even a confirmed absence is reported as a
80
+ * lookup result rather than as a missing record — Emit mirrors records and never repairs server data.
81
+ */
82
+ export function findUnmirroredStorageKeys(referenced, localMirrorKeys, remoteIndex) {
83
+ if (!remoteIndex.complete)
84
+ return [];
85
+ const mirrored = new Set(localMirrorKeys);
86
+ const unresolved = new Set();
87
+ for (const key of referenced) {
88
+ if (mirrored.has(key) || remoteIndex.storageKeys.has(key))
89
+ continue;
90
+ unresolved.add(key);
91
+ }
92
+ return Array.from(unresolved).sort();
93
+ }
94
+ //# sourceMappingURL=remote_index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote_index.js","sourceRoot":"","sources":["../../../src/sync/media/remote_index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAoD,MAAM,YAAY,CAAC;AAEpG,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,OAQ9C;IACC,MAAM,OAAO,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,oBAAoB,CAAC;IACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,QAAQ,CAAC;IAE9C,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IACtC,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAAkB,CAAC;IACvD,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAA2B,CAAC;IAC/D,MAAM,WAAW,GAAG,IAAI,GAAG,EAA2B,CAAC;IACvD,IAAI,UAAU,GAAkB,IAAI,CAAC;IACrC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,MAAM,GAAuB,SAAS,CAAC;IAE3C,SAAS,CAAC;QACR,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;QACrC,IAAI,SAAS,IAAI,CAAC;YAAE,MAAM;QAC1B,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAEpD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,oBAAoB,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QAC1D,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;QACpD,IAAI,MAAM;YAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAEnD,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE;YACxC,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,OAAO,CAAC,KAAK,EAAE;gBACxC,MAAM,EAAE,kBAAkB;aAC3B;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,QAAQ,CAAC;gBACjB,OAAO,EAAE,yBAAyB,GAAG,CAAC,UAAU,EAAE;gBAClD,IAAI,EAAE,mBAAmB;gBACzB,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE;gBACxB,UAAU,EAAE,kDAAkD;aAC/D,CAAC,CAAC;QACL,CAAC;QAED,MAAM,IAAI,GAAG,cAAc,CAA0B,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QACvE,IAAI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ;YAAE,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QACtE,MAAM,QAAQ,GAAG,CACf,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAC9D,CAAC;QAEvB,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;YAC5B,OAAO,EAAE,CAAC;YACV,IAAI,IAAI,CAAC,EAAE;gBAAE,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,CAAC;YAC1D,IAAI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;gBAC3D,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;gBACjC,8EAA8E;gBAC9E,IAAI,IAAI,CAAC,EAAE,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;oBAC1D,oBAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;gBACrD,CAAC;gBACD,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;oBAC9C,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;gBACjD,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACrF,QAAQ,GAAG,IAAI,CAAC;YAChB,MAAM;QACR,CAAC;QACD,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC;IAC3B,CAAC;IAED,OAAO;QACL,WAAW;QACX,oBAAoB;QACpB,mBAAmB;QACnB,WAAW;QACX,UAAU;QACV,QAAQ;QACR,YAAY,EAAE,OAAO;KACtB,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CACvC,UAA4B,EAC5B,eAAiC,EACjC,WAAkC;IAElC,IAAI,CAAC,WAAW,CAAC,QAAQ;QAAE,OAAO,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,eAAe,CAAC,CAAC;IAC1C,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;IACrC,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,WAAW,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QACpE,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC;AACvC,CAAC"}