@mcp-consultant-tools/azure-devops 30.0.0-beta.9 → 31.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/build/azure-devops-client.d.ts.map +1 -1
  2. package/build/azure-devops-client.js +23 -3
  3. package/build/azure-devops-client.js.map +1 -1
  4. package/build/cli/commands/index.d.ts +1 -0
  5. package/build/cli/commands/index.d.ts.map +1 -1
  6. package/build/cli/commands/index.js +3 -0
  7. package/build/cli/commands/index.js.map +1 -1
  8. package/build/cli/commands/test-commands.d.ts +7 -0
  9. package/build/cli/commands/test-commands.d.ts.map +1 -0
  10. package/build/cli/commands/test-commands.js +131 -0
  11. package/build/cli/commands/test-commands.js.map +1 -0
  12. package/build/cli/commands/work-item-commands.d.ts.map +1 -1
  13. package/build/cli/commands/work-item-commands.js +34 -0
  14. package/build/cli/commands/work-item-commands.js.map +1 -1
  15. package/build/context-factory.d.ts.map +1 -1
  16. package/build/context-factory.js +10 -4
  17. package/build/context-factory.js.map +1 -1
  18. package/build/schemas.d.ts +13 -0
  19. package/build/schemas.d.ts.map +1 -0
  20. package/build/schemas.js +47 -0
  21. package/build/schemas.js.map +1 -0
  22. package/build/services/checklist-service.d.ts.map +1 -1
  23. package/build/services/checklist-service.js +3 -0
  24. package/build/services/checklist-service.js.map +1 -1
  25. package/build/services/index.d.ts +1 -0
  26. package/build/services/index.d.ts.map +1 -1
  27. package/build/services/index.js +1 -0
  28. package/build/services/index.js.map +1 -1
  29. package/build/services/sync-service.d.ts.map +1 -1
  30. package/build/services/sync-service.js +75 -14
  31. package/build/services/sync-service.js.map +1 -1
  32. package/build/services/test-service.d.ts +106 -0
  33. package/build/services/test-service.d.ts.map +1 -0
  34. package/build/services/test-service.js +245 -0
  35. package/build/services/test-service.js.map +1 -0
  36. package/build/services/work-item-service.d.ts +29 -1
  37. package/build/services/work-item-service.d.ts.map +1 -1
  38. package/build/services/work-item-service.js +66 -6
  39. package/build/services/work-item-service.js.map +1 -1
  40. package/build/sync/annotation-parser.d.ts +52 -0
  41. package/build/sync/annotation-parser.d.ts.map +1 -0
  42. package/build/sync/annotation-parser.js +83 -0
  43. package/build/sync/annotation-parser.js.map +1 -0
  44. package/build/sync/field-aliases.d.ts +35 -0
  45. package/build/sync/field-aliases.d.ts.map +1 -0
  46. package/build/sync/field-aliases.js +76 -0
  47. package/build/sync/field-aliases.js.map +1 -0
  48. package/build/sync/html-converter.d.ts.map +1 -1
  49. package/build/sync/html-converter.js +12 -2
  50. package/build/sync/html-converter.js.map +1 -1
  51. package/build/sync/html-detection.d.ts +18 -65
  52. package/build/sync/html-detection.d.ts.map +1 -1
  53. package/build/sync/html-detection.js +72 -113
  54. package/build/sync/html-detection.js.map +1 -1
  55. package/build/sync/image-handler.d.ts +66 -0
  56. package/build/sync/image-handler.d.ts.map +1 -0
  57. package/build/sync/image-handler.js +135 -0
  58. package/build/sync/image-handler.js.map +1 -0
  59. package/build/sync/image-manifest.d.ts +66 -0
  60. package/build/sync/image-manifest.d.ts.map +1 -0
  61. package/build/sync/image-manifest.js +96 -0
  62. package/build/sync/image-manifest.js.map +1 -0
  63. package/build/sync/image-sync.d.ts +88 -0
  64. package/build/sync/image-sync.d.ts.map +1 -0
  65. package/build/sync/image-sync.js +274 -0
  66. package/build/sync/image-sync.js.map +1 -0
  67. package/build/sync/index.d.ts +7 -0
  68. package/build/sync/index.d.ts.map +1 -1
  69. package/build/sync/index.js +7 -0
  70. package/build/sync/index.js.map +1 -1
  71. package/build/sync/legacy-mappings.d.ts +37 -0
  72. package/build/sync/legacy-mappings.d.ts.map +1 -0
  73. package/build/sync/legacy-mappings.js +75 -0
  74. package/build/sync/legacy-mappings.js.map +1 -0
  75. package/build/sync/markdown-serializer.d.ts +54 -60
  76. package/build/sync/markdown-serializer.d.ts.map +1 -1
  77. package/build/sync/markdown-serializer.js +607 -545
  78. package/build/sync/markdown-serializer.js.map +1 -1
  79. package/build/sync/task-serializer.d.ts.map +1 -1
  80. package/build/sync/task-serializer.js +46 -8
  81. package/build/sync/task-serializer.js.map +1 -1
  82. package/build/sync/template-loader.d.ts +56 -0
  83. package/build/sync/template-loader.d.ts.map +1 -0
  84. package/build/sync/template-loader.js +138 -0
  85. package/build/sync/template-loader.js.map +1 -0
  86. package/build/sync/templates/bug.md +25 -0
  87. package/build/sync/templates/epic.md +23 -0
  88. package/build/sync/templates/feature.md +23 -0
  89. package/build/sync/templates/task.md +14 -0
  90. package/build/sync/templates/user-story.md +26 -0
  91. package/build/tool-examples.d.ts +20 -0
  92. package/build/tool-examples.d.ts.map +1 -1
  93. package/build/tool-examples.js +41 -0
  94. package/build/tool-examples.js.map +1 -1
  95. package/build/tools/build-tools.d.ts.map +1 -1
  96. package/build/tools/build-tools.js +7 -6
  97. package/build/tools/build-tools.js.map +1 -1
  98. package/build/tools/checklist-tools.d.ts.map +1 -1
  99. package/build/tools/checklist-tools.js +6 -5
  100. package/build/tools/checklist-tools.js.map +1 -1
  101. package/build/tools/index.d.ts +1 -0
  102. package/build/tools/index.d.ts.map +1 -1
  103. package/build/tools/index.js +5 -2
  104. package/build/tools/index.js.map +1 -1
  105. package/build/tools/pull-request-tools.d.ts.map +1 -1
  106. package/build/tools/pull-request-tools.js +15 -14
  107. package/build/tools/pull-request-tools.js.map +1 -1
  108. package/build/tools/sync-tools.d.ts.map +1 -1
  109. package/build/tools/sync-tools.js +9 -8
  110. package/build/tools/sync-tools.js.map +1 -1
  111. package/build/tools/test-tools.d.ts +3 -0
  112. package/build/tools/test-tools.d.ts.map +1 -0
  113. package/build/tools/test-tools.js +184 -0
  114. package/build/tools/test-tools.js.map +1 -0
  115. package/build/tools/variable-group-tools.d.ts.map +1 -1
  116. package/build/tools/variable-group-tools.js +2 -1
  117. package/build/tools/variable-group-tools.js.map +1 -1
  118. package/build/tools/visualize-tools.d.ts.map +1 -1
  119. package/build/tools/visualize-tools.js +2 -1
  120. package/build/tools/visualize-tools.js.map +1 -1
  121. package/build/tools/wiki-tools.d.ts.map +1 -1
  122. package/build/tools/wiki-tools.js +4 -3
  123. package/build/tools/wiki-tools.js.map +1 -1
  124. package/build/tools/work-item-tools.d.ts.map +1 -1
  125. package/build/tools/work-item-tools.js +42 -11
  126. package/build/tools/work-item-tools.js.map +1 -1
  127. package/build/types.d.ts +2 -0
  128. package/build/types.d.ts.map +1 -1
  129. package/package.json +3 -3
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Image Reference Handler
3
+ *
4
+ * Detects and rewrites image references in ADO work item content.
5
+ *
6
+ * Two source forms are recognised:
7
+ * - HTML <img src="..."> (raw HTML, typically pre-conversion)
8
+ * - Markdown ![alt](src) (post Turndown conversion or hand-written)
9
+ *
10
+ * Each <img>/![] reference is parsed; if the URL points at an ADO work item
11
+ * attachment (`_apis/wit/attachments/{guid}`), we extract the GUID, fileName,
12
+ * and project GUID. Callers can then download the file and rewrite the source
13
+ * to a local relative path — or, on push, walk the manifest and rewrite local
14
+ * paths back to the original ADO URL.
15
+ */
16
+ /**
17
+ * Parse an ADO work item attachment URL.
18
+ *
19
+ * Recognised patterns:
20
+ * https://dev.azure.com/{org}/{projectGuid}/_apis/wit/attachments/{guid}?fileName=image.png&...
21
+ * https://dev.azure.com/{org}/_apis/wit/attachments/{guid}?fileName=image.png&...
22
+ *
23
+ * Returns null when the URL is not an ADO attachment URL.
24
+ */
25
+ export function parseAdoAttachmentUrl(url) {
26
+ // Match _apis/wit/attachments/{guid} anywhere in the URL.
27
+ // GUID = 8-4-4-4-12 hex chars
28
+ const guidPattern = /\/_apis\/wit\/attachments\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[/?#]|$)/i;
29
+ const guidMatch = url.match(guidPattern);
30
+ if (!guidMatch)
31
+ return null;
32
+ const guid = guidMatch[1];
33
+ // Try to extract project GUID (segment between org and _apis).
34
+ // dev.azure.com/{org}/{projectGuid}/_apis/...
35
+ let projectGuid;
36
+ const projectGuidPattern = /dev\.azure\.com\/[^/]+\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/_apis\//i;
37
+ const projectMatch = url.match(projectGuidPattern);
38
+ if (projectMatch) {
39
+ projectGuid = projectMatch[1];
40
+ }
41
+ // Extract fileName from query string; fall back to a sensible default.
42
+ let fileName = 'attachment.bin';
43
+ try {
44
+ const u = new URL(url);
45
+ const qsName = u.searchParams.get('fileName');
46
+ if (qsName)
47
+ fileName = qsName;
48
+ }
49
+ catch {
50
+ const qsMatch = url.match(/[?&]fileName=([^&#]+)/);
51
+ if (qsMatch) {
52
+ try {
53
+ fileName = decodeURIComponent(qsMatch[1]);
54
+ }
55
+ catch {
56
+ fileName = qsMatch[1];
57
+ }
58
+ }
59
+ }
60
+ return { guid, fileName, projectGuid, originalUrl: url };
61
+ }
62
+ /**
63
+ * Extract every image reference from a string of HTML or markdown content.
64
+ *
65
+ * Returns refs in source order. Both HTML <img> tags and markdown ![]() are
66
+ * detected. Refs whose URL parses as an ADO attachment have `adoAttachment`
67
+ * populated.
68
+ */
69
+ export function extractImageRefs(content) {
70
+ if (!content)
71
+ return [];
72
+ const refs = [];
73
+ // HTML <img src="..."> — handles both " and ' delimiters and self-closing
74
+ const htmlRegex = /<img[^>]*\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)')[^>]*\/?>/gi;
75
+ let m;
76
+ while ((m = htmlRegex.exec(content)) !== null) {
77
+ const src = m[1] || m[2];
78
+ if (!src)
79
+ continue;
80
+ refs.push({
81
+ originalSrc: src,
82
+ isHtmlTag: true,
83
+ index: m.index,
84
+ length: m[0].length,
85
+ adoAttachment: parseAdoAttachmentUrl(src) ?? undefined,
86
+ });
87
+ }
88
+ // Markdown ![alt](url) — url terminates at whitespace or close paren
89
+ const mdRegex = /!\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
90
+ while ((m = mdRegex.exec(content)) !== null) {
91
+ const src = m[1];
92
+ if (!src)
93
+ continue;
94
+ refs.push({
95
+ originalSrc: src,
96
+ isHtmlTag: false,
97
+ index: m.index,
98
+ length: m[0].length,
99
+ adoAttachment: parseAdoAttachmentUrl(src) ?? undefined,
100
+ });
101
+ }
102
+ return refs.sort((a, b) => a.index - b.index);
103
+ }
104
+ /**
105
+ * Rewrite image src/url values in content using a mapper function.
106
+ *
107
+ * The mapper receives the original src and the parsed ADO attachment (if any)
108
+ * and returns the new src to substitute. Returning null/undefined leaves the
109
+ * original src in place.
110
+ *
111
+ * Both HTML <img src="..."> and markdown ![](src) forms are rewritten.
112
+ */
113
+ export function rewriteImageSrcs(content, mapper) {
114
+ if (!content)
115
+ return content;
116
+ // Rewrite HTML <img src="...">
117
+ let result = content.replace(/(<img[^>]*\bsrc\s*=\s*)(?:"([^"]+)"|'([^']+)')([^>]*\/?>)/gi, (_match, before, dq, sq, after) => {
118
+ const src = dq ?? sq;
119
+ const ado = parseAdoAttachmentUrl(src) ?? undefined;
120
+ const newSrc = mapper(src, ado);
121
+ const finalSrc = newSrc ?? src;
122
+ // Preserve the original quote style
123
+ const quote = dq !== undefined ? '"' : "'";
124
+ return `${before}${quote}${finalSrc}${quote}${after}`;
125
+ });
126
+ // Rewrite markdown ![alt](url "optional title")
127
+ result = result.replace(/(!\[[^\]]*\]\()([^)\s]+)((?:\s+"[^"]*")?\))/g, (_match, before, src, after) => {
128
+ const ado = parseAdoAttachmentUrl(src) ?? undefined;
129
+ const newSrc = mapper(src, ado);
130
+ const finalSrc = newSrc ?? src;
131
+ return `${before}${finalSrc}${after}`;
132
+ });
133
+ return result;
134
+ }
135
+ //# sourceMappingURL=image-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-handler.js","sourceRoot":"","sources":["../../src/sync/image-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Image Attachment Manifest
3
+ *
4
+ * Per-work-item record of attachment files synced between local disk and ADO.
5
+ * Stored at `{syncFolder}/{workItemId}/.attachments.json`.
6
+ *
7
+ * The manifest is the source of truth for "is this image already on ADO?":
8
+ * - On pull: each downloaded ADO attachment is recorded with `source: "ado-pull"`.
9
+ * - On push: when a local image src matches a manifest entry, the original
10
+ * ADO URL is reused (no re-upload). When it doesn't match, the file is
11
+ * uploaded as a new attachment and added with `source: "local-uploaded"`.
12
+ */
13
+ export type AttachmentSource = 'ado-pull' | 'local-uploaded';
14
+ export interface AttachmentManifestEntry {
15
+ /** ADO attachment GUID */
16
+ guid: string;
17
+ /** Original filename as stored in ADO */
18
+ fileName: string;
19
+ /** Path relative to the work item folder (e.g. "attachments/{guid}-image.png") */
20
+ localPath: string;
21
+ /** Original ADO attachment URL (used to re-reference on push) */
22
+ originalUrl: string;
23
+ /** Where the entry came from */
24
+ source: AttachmentSource;
25
+ /** ADO field reference name where the image was first seen (informational) */
26
+ field?: string;
27
+ /** ISO timestamp of upload (for local-uploaded entries) */
28
+ uploadedAt?: string;
29
+ }
30
+ export interface AttachmentManifest {
31
+ workItemId: number;
32
+ lastSyncedAt: string;
33
+ attachments: AttachmentManifestEntry[];
34
+ }
35
+ /** Folder for a work item's attachments + manifest. */
36
+ export declare function getWorkItemAttachmentDir(syncFolder: string, workItemId: number): string;
37
+ /** Folder where the binary attachment files live. */
38
+ export declare function getAttachmentsBinDir(syncFolder: string, workItemId: number): string;
39
+ /** Path to the manifest JSON file. */
40
+ export declare function getManifestPath(syncFolder: string, workItemId: number): string;
41
+ /**
42
+ * Build the local relative path for a downloaded attachment.
43
+ * Example: `attachments/5e5c125f-...-image.png`.
44
+ *
45
+ * The GUID prefix keeps filenames unique even when ADO returns generic names
46
+ * like "image.png" for many different attachments.
47
+ */
48
+ export declare function buildLocalAttachmentPath(guid: string, fileName: string): string;
49
+ /** Read manifest from disk. Returns an empty manifest if the file is missing. */
50
+ export declare function readManifest(syncFolder: string, workItemId: number): Promise<AttachmentManifest>;
51
+ /** Write manifest to disk, creating the parent directory if needed. */
52
+ export declare function writeManifest(syncFolder: string, manifest: AttachmentManifest): Promise<void>;
53
+ /**
54
+ * Add (or replace) an entry in the manifest. Lookup is by `guid` — the same
55
+ * attachment is never recorded twice. Returns the manifest unchanged when an
56
+ * existing entry has the same guid AND localPath, otherwise replaces it.
57
+ */
58
+ export declare function upsertEntry(manifest: AttachmentManifest, entry: AttachmentManifestEntry): AttachmentManifest;
59
+ /**
60
+ * Look up a manifest entry by its local path (relative to the work item folder).
61
+ * Used on push to recognise images that came from ADO and avoid re-uploading.
62
+ */
63
+ export declare function findEntryByLocalPath(manifest: AttachmentManifest, localPath: string): AttachmentManifestEntry | undefined;
64
+ /** Look up an entry by its original ADO URL. */
65
+ export declare function findEntryByAdoUrl(manifest: AttachmentManifest, url: string): AttachmentManifestEntry | undefined;
66
+ //# sourceMappingURL=image-manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-manifest.d.ts","sourceRoot":"","sources":["../../src/sync/image-manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,gBAAgB,CAAC;AAE7D,MAAM,WAAW,uBAAuB;IACtC,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,MAAM,EAAE,gBAAgB,CAAC;IACzB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,uBAAuB,EAAE,CAAC;CACxC;AAKD,uDAAuD;AACvD,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEvF;AAED,qDAAqD;AACrD,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED,sCAAsC;AACtC,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED,iFAAiF;AACjF,wBAAsB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAiBtG;AAED,uEAAuE;AACvE,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMnG;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,kBAAkB,EAAE,KAAK,EAAE,uBAAuB,GAAG,kBAAkB,CAQ5G;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,GAAG,uBAAuB,GAAG,SAAS,CAKzH;AAED,gDAAgD;AAChD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,GAAG,uBAAuB,GAAG,SAAS,CAEhH"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Image Attachment Manifest
3
+ *
4
+ * Per-work-item record of attachment files synced between local disk and ADO.
5
+ * Stored at `{syncFolder}/{workItemId}/.attachments.json`.
6
+ *
7
+ * The manifest is the source of truth for "is this image already on ADO?":
8
+ * - On pull: each downloaded ADO attachment is recorded with `source: "ado-pull"`.
9
+ * - On push: when a local image src matches a manifest entry, the original
10
+ * ADO URL is reused (no re-upload). When it doesn't match, the file is
11
+ * uploaded as a new attachment and added with `source: "local-uploaded"`.
12
+ */
13
+ import { promises as fs } from 'node:fs';
14
+ import * as path from 'node:path';
15
+ const MANIFEST_FILENAME = '.attachments.json';
16
+ const ATTACHMENTS_SUBDIR = 'attachments';
17
+ /** Folder for a work item's attachments + manifest. */
18
+ export function getWorkItemAttachmentDir(syncFolder, workItemId) {
19
+ return path.join(syncFolder, String(workItemId));
20
+ }
21
+ /** Folder where the binary attachment files live. */
22
+ export function getAttachmentsBinDir(syncFolder, workItemId) {
23
+ return path.join(getWorkItemAttachmentDir(syncFolder, workItemId), ATTACHMENTS_SUBDIR);
24
+ }
25
+ /** Path to the manifest JSON file. */
26
+ export function getManifestPath(syncFolder, workItemId) {
27
+ return path.join(getWorkItemAttachmentDir(syncFolder, workItemId), MANIFEST_FILENAME);
28
+ }
29
+ /**
30
+ * Build the local relative path for a downloaded attachment.
31
+ * Example: `attachments/5e5c125f-...-image.png`.
32
+ *
33
+ * The GUID prefix keeps filenames unique even when ADO returns generic names
34
+ * like "image.png" for many different attachments.
35
+ */
36
+ export function buildLocalAttachmentPath(guid, fileName) {
37
+ return path.posix.join(ATTACHMENTS_SUBDIR, `${guid}-${fileName}`);
38
+ }
39
+ /** Read manifest from disk. Returns an empty manifest if the file is missing. */
40
+ export async function readManifest(syncFolder, workItemId) {
41
+ const manifestPath = getManifestPath(syncFolder, workItemId);
42
+ try {
43
+ const raw = await fs.readFile(manifestPath, 'utf-8');
44
+ const parsed = JSON.parse(raw);
45
+ if (typeof parsed !== 'object' || parsed === null || !Array.isArray(parsed.attachments)) {
46
+ throw new Error('Malformed manifest');
47
+ }
48
+ return parsed;
49
+ }
50
+ catch (err) {
51
+ if (err.code === 'ENOENT') {
52
+ return { workItemId, lastSyncedAt: new Date().toISOString(), attachments: [] };
53
+ }
54
+ // Corrupted manifest — start fresh rather than failing the sync.
55
+ console.error(`Warning: could not parse ${manifestPath}: ${err.message}. Starting fresh.`);
56
+ return { workItemId, lastSyncedAt: new Date().toISOString(), attachments: [] };
57
+ }
58
+ }
59
+ /** Write manifest to disk, creating the parent directory if needed. */
60
+ export async function writeManifest(syncFolder, manifest) {
61
+ const dir = getWorkItemAttachmentDir(syncFolder, manifest.workItemId);
62
+ await fs.mkdir(dir, { recursive: true });
63
+ const manifestPath = getManifestPath(syncFolder, manifest.workItemId);
64
+ manifest.lastSyncedAt = new Date().toISOString();
65
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
66
+ }
67
+ /**
68
+ * Add (or replace) an entry in the manifest. Lookup is by `guid` — the same
69
+ * attachment is never recorded twice. Returns the manifest unchanged when an
70
+ * existing entry has the same guid AND localPath, otherwise replaces it.
71
+ */
72
+ export function upsertEntry(manifest, entry) {
73
+ const existing = manifest.attachments.findIndex(a => a.guid === entry.guid);
74
+ if (existing >= 0) {
75
+ manifest.attachments[existing] = { ...manifest.attachments[existing], ...entry };
76
+ }
77
+ else {
78
+ manifest.attachments.push(entry);
79
+ }
80
+ return manifest;
81
+ }
82
+ /**
83
+ * Look up a manifest entry by its local path (relative to the work item folder).
84
+ * Used on push to recognise images that came from ADO and avoid re-uploading.
85
+ */
86
+ export function findEntryByLocalPath(manifest, localPath) {
87
+ // Normalise both sides: strip leading `./` and convert backslashes.
88
+ const normalise = (p) => p.replace(/\\/g, '/').replace(/^\.\//, '');
89
+ const target = normalise(localPath);
90
+ return manifest.attachments.find(a => normalise(a.localPath) === target);
91
+ }
92
+ /** Look up an entry by its original ADO URL. */
93
+ export function findEntryByAdoUrl(manifest, url) {
94
+ return manifest.attachments.find(a => a.originalUrl === url);
95
+ }
96
+ //# sourceMappingURL=image-manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-manifest.js","sourceRoot":"","sources":["../../src/sync/image-manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Image Sync Orchestration
3
+ *
4
+ * Hooks `image-handler` + `image-manifest` + `WorkItemService.downloadAttachment`
5
+ * into the work-item pull/push pipelines.
6
+ *
7
+ * - On pull: scan all string fields + comment bodies for ADO attachment <img>
8
+ * refs, download each binary into `{folder}/{id}/attachments/`, rewrite the
9
+ * src to the local relative path, and write the manifest.
10
+ *
11
+ * - On push: read the manifest, scan content for image refs, rewrite local
12
+ * paths back to original ADO URLs, upload any new local files, append to
13
+ * the manifest.
14
+ */
15
+ import { type AttachmentManifest } from './image-manifest.js';
16
+ interface ImageDownloadService {
17
+ downloadAttachment(project: string, attachmentGuid: string, urlFileName: string, outputDir: string, outputFileName?: string): Promise<{
18
+ filePath: string;
19
+ fileName: string;
20
+ size: number;
21
+ guid: string;
22
+ }>;
23
+ }
24
+ interface ImageUploadService {
25
+ uploadAttachment(project: string, filePath: string, fileName?: string): Promise<{
26
+ id: string;
27
+ url: string;
28
+ fileName: string;
29
+ size: number;
30
+ }>;
31
+ }
32
+ export interface PullImageResult {
33
+ downloaded: number;
34
+ reused: number;
35
+ failed: Array<{
36
+ guid: string;
37
+ fileName: string;
38
+ error: string;
39
+ }>;
40
+ manifest: AttachmentManifest;
41
+ }
42
+ /**
43
+ * Process all string fields on a work item, downloading attachments and
44
+ * rewriting srcs. Mutates `workItem.fields` in place. Persists the manifest.
45
+ */
46
+ export declare function pullWorkItemImages(workItem: any, project: string, syncFolder: string, service: ImageDownloadService): Promise<PullImageResult>;
47
+ /**
48
+ * Process comment bodies for image refs. Mutates `comments[i].text` in place.
49
+ * Uses the same manifest as the parent work item so attachments are tracked
50
+ * together. Returns aggregated counts.
51
+ */
52
+ export declare function pullCommentImages(workItemId: number, comments: Array<{
53
+ text?: string;
54
+ content?: string;
55
+ }>, project: string, syncFolder: string, service: ImageDownloadService): Promise<PullImageResult>;
56
+ export interface PushImageResult {
57
+ uploaded: number;
58
+ reused: number;
59
+ failed: Array<{
60
+ src: string;
61
+ error: string;
62
+ }>;
63
+ manifest: AttachmentManifest;
64
+ }
65
+ /**
66
+ * Apply image push processing to a parsed work item file's body fields.
67
+ * Iterates every refname in `bodyFieldMap`, so custom body fields (not just
68
+ * the historical Description/ReproSteps/AC/custom-four) get image handling.
69
+ * Mutates the parsed object in-place and persists the manifest.
70
+ */
71
+ export declare function pushWorkItemImages(parsed: {
72
+ bodyFieldMap: Record<string, string>;
73
+ description?: string;
74
+ reproSteps?: string;
75
+ acceptanceCriteria?: string;
76
+ additionalFields?: any;
77
+ }, workItemId: number, project: string, syncFolder: string, service: ImageUploadService): Promise<PushImageResult>;
78
+ /**
79
+ * Append a manually-uploaded attachment to a work item's manifest.
80
+ * Used by the standalone `upload-work-item-attachment` tool.
81
+ */
82
+ export declare function recordExternalUpload(syncFolder: string, workItemId: number, uploaded: {
83
+ id: string;
84
+ url: string;
85
+ fileName: string;
86
+ }, localPath: string): Promise<void>;
87
+ export {};
88
+ //# sourceMappingURL=image-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-sync.d.ts","sourceRoot":"","sources":["../../src/sync/image-sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAUH,OAAO,EAQL,KAAK,kBAAkB,EAExB,MAAM,qBAAqB,CAAC;AAa7B,UAAU,oBAAoB;IAC5B,kBAAkB,CAChB,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChF;AAED,UAAU,kBAAkB;IAC1B,gBAAgB,CACd,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACzE;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjE,QAAQ,EAAE,kBAAkB,CAAC;CAC9B;AAwED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,GAAG,EACb,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,eAAe,CAAC,CAqB1B;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EACpD,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,eAAe,CAAC,CAsB1B;AAMD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9C,QAAQ,EAAE,kBAAkB,CAAC;CAC9B;AAwHD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE;IACN,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,GAAG,CAAC;CACxB,EACD,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,eAAe,CAAC,CA2B1B;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EACvD,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAYf"}