@mcoda/integrations 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/CHANGELOG.md CHANGED
@@ -3,5 +3,8 @@
3
3
  ## Unreleased
4
4
  - Initial public packaging for @mcoda/integrations.
5
5
 
6
+ ## 0.1.9
7
+ - Release v0.1.9.
8
+
6
9
  ## 0.1.8
7
10
  - Initial release.
package/README.md CHANGED
@@ -7,7 +7,7 @@ External integrations for mcoda (docdex, telemetry, VCS, QA runners, update chec
7
7
  - Install: `npm i @mcoda/integrations`
8
8
 
9
9
  ## What it provides
10
- - DocdexClient for local or remote docdex queries.
10
+ - DocdexClient for docdex daemon queries and CLI-backed ingestion.
11
11
  - TelemetryClient for token usage reporting.
12
12
  - VcsClient for Git operations.
13
13
  - SystemClient for update checks.
@@ -26,6 +26,8 @@ const docs = await client.search({ docType: "rfp", query: "payments" });
26
26
  ```
27
27
 
28
28
  ## Notes
29
+ - Docdex state lives under `~/.docdex` (managed by the `docdex` CLI); mcoda does not create repo-local `.docdex`.
30
+ - Chromium QA expects Playwright browsers provisioned via `docdex setup`.
29
31
  - Some integrations call external services; configure base URLs and tokens as needed.
30
32
  - Primarily used by the mcoda CLI; APIs may evolve.
31
33
 
@@ -25,17 +25,21 @@ export interface RegisterDocumentInput {
25
25
  }
26
26
  export declare class DocdexClient {
27
27
  private options;
28
+ private resolvedBaseUrl?;
29
+ private repoId?;
30
+ private initializing;
28
31
  constructor(options?: {
29
32
  workspaceRoot?: string;
30
- storePath?: string;
31
33
  baseUrl?: string;
32
34
  authToken?: string;
35
+ repoId?: string;
33
36
  });
34
- private getStorePath;
35
37
  private normalizePath;
36
- private loadStore;
38
+ private resolveBaseUrl;
39
+ private ensureRepoInitialized;
37
40
  private fetchRemote;
38
- private saveStore;
41
+ private buildLocalDoc;
42
+ private coerceSearchResults;
39
43
  fetchDocumentById(id: string): Promise<DocdexDocument>;
40
44
  findDocumentByPath(docPath: string, docType?: string): Promise<DocdexDocument | undefined>;
41
45
  search(filter: {
@@ -44,6 +48,7 @@ export declare class DocdexClient {
44
48
  query?: string;
45
49
  profile?: string;
46
50
  }): Promise<DocdexDocument[]>;
51
+ reindex(): Promise<void>;
47
52
  registerDocument(input: RegisterDocumentInput): Promise<DocdexDocument>;
48
53
  ensureRegisteredFromFile(docPath: string, docType: string, metadata?: Record<string, unknown>): Promise<DocdexDocument>;
49
54
  }
@@ -1 +1 @@
1
- {"version":3,"file":"DocdexClient.d.ts","sourceRoot":"","sources":["../../src/docdex/DocdexClient.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAyCD,qBAAa,YAAY;IAErB,OAAO,CAAC,OAAO;gBAAP,OAAO,GAAE;QACf,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;KACf;IAGR,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,aAAa;YAYP,SAAS;YAaT,WAAW;YAYX,SAAS;IAOjB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAoBtD,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAU1F,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IA0BtH,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC;IA6DvE,wBAAwB,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,OAAO,CAAC,cAAc,CAAC;CAO3B"}
1
+ {"version":3,"file":"DocdexClient.d.ts","sourceRoot":"","sources":["../../src/docdex/DocdexClient.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAkDD,qBAAa,YAAY;IAMrB,OAAO,CAAC,OAAO;IALjB,OAAO,CAAC,eAAe,CAAC,CAAS;IACjC,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,YAAY,CAAS;gBAGnB,OAAO,GAAE;QACf,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC;KACZ;IAKR,OAAO,CAAC,aAAa;YAYP,cAAc;YAWd,qBAAqB;YA0BrB,WAAW;IA8BzB,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,mBAAmB;IAmDrB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IA4BtD,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAS1F,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAkBtH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAcxB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC;IA0BvE,wBAAwB,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,OAAO,CAAC,cAAc,CAAC;CAoB3B"}
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import { promises as fs } from "node:fs";
4
+ import { resolveDocdexBaseUrl, runDocdex } from "./DocdexRuntime.js";
4
5
  const nowIso = () => new Date().toISOString();
5
6
  const segmentize = (docId, content) => {
6
7
  const lines = content.split(/\r?\n/);
@@ -33,15 +34,30 @@ const segmentize = (docId, content) => {
33
34
  flush();
34
35
  return segments;
35
36
  };
37
+ const inferDocType = (docPath, fallback = "DOC") => {
38
+ if (!docPath)
39
+ return fallback;
40
+ const name = path.basename(docPath).toLowerCase();
41
+ if (name.includes("openapi") || name.includes("swagger"))
42
+ return "OPENAPI";
43
+ if (name.includes("sds"))
44
+ return "SDS";
45
+ if (name.includes("pdr"))
46
+ return "PDR";
47
+ if (name.includes("rfp"))
48
+ return "RFP";
49
+ return fallback;
50
+ };
51
+ const normalizeBaseUrl = (value) => {
52
+ if (!value)
53
+ return value;
54
+ return value.endsWith("/") ? value.slice(0, -1) : value;
55
+ };
36
56
  export class DocdexClient {
37
57
  constructor(options = {}) {
38
58
  this.options = options;
39
- }
40
- getStorePath() {
41
- const base = this.options.storePath
42
- ? path.resolve(this.options.storePath)
43
- : path.join(this.options.workspaceRoot ?? process.cwd(), ".mcoda", "docdex", "documents.json");
44
- return base;
59
+ this.initializing = false;
60
+ this.repoId = options.repoId;
45
61
  }
46
62
  normalizePath(inputPath) {
47
63
  if (!inputPath)
@@ -55,162 +71,264 @@ export class DocdexClient {
55
71
  }
56
72
  return absolute;
57
73
  }
58
- async loadStore() {
59
- const storePath = this.getStorePath();
74
+ async resolveBaseUrl() {
75
+ if (this.options.baseUrl !== undefined) {
76
+ const trimmed = this.options.baseUrl.trim();
77
+ return trimmed ? normalizeBaseUrl(trimmed) : undefined;
78
+ }
79
+ if (this.resolvedBaseUrl !== undefined)
80
+ return this.resolvedBaseUrl;
81
+ const resolved = await resolveDocdexBaseUrl({ cwd: this.options.workspaceRoot });
82
+ this.resolvedBaseUrl = resolved ? normalizeBaseUrl(resolved) : undefined;
83
+ return this.resolvedBaseUrl;
84
+ }
85
+ async ensureRepoInitialized(baseUrl, force = false) {
86
+ if ((this.repoId && !force) || this.initializing)
87
+ return;
88
+ if (!this.options.workspaceRoot)
89
+ return;
90
+ this.initializing = true;
60
91
  try {
61
- const raw = await fs.readFile(storePath, "utf8");
62
- const parsed = JSON.parse(raw);
63
- parsed.documents = parsed.documents ?? [];
64
- parsed.segments = parsed.segments ?? [];
65
- return parsed;
92
+ const rootUri = `file://${path.resolve(this.options.workspaceRoot)}`;
93
+ const response = await fetch(`${baseUrl}/v1/initialize`, {
94
+ method: "POST",
95
+ headers: { "Content-Type": "application/json" },
96
+ body: JSON.stringify({ rootUri }),
97
+ });
98
+ if (!response.ok)
99
+ return;
100
+ const payload = (await response.json().catch(() => undefined));
101
+ const repoId = payload?.repoId ??
102
+ payload?.repo_id ??
103
+ payload?.repo ??
104
+ payload?.id;
105
+ if (repoId)
106
+ this.repoId = String(repoId);
66
107
  }
67
108
  catch {
68
- return { updatedAt: nowIso(), documents: [], segments: [] };
109
+ // ignore initialize errors; assume single-repo daemon
110
+ }
111
+ finally {
112
+ this.initializing = false;
69
113
  }
70
114
  }
71
115
  async fetchRemote(pathname, init) {
72
- if (!this.options.baseUrl)
73
- throw new Error("Docdex baseUrl not configured");
74
- const url = new URL(pathname, this.options.baseUrl);
75
- const headers = { "Content-Type": "application/json" };
76
- if (this.options.authToken)
77
- headers.authorization = `Bearer ${this.options.authToken}`;
78
- const response = await fetch(url, { ...init, headers: { ...headers, ...init?.headers } });
116
+ const baseUrl = await this.resolveBaseUrl();
117
+ if (!baseUrl) {
118
+ throw new Error("Docdex baseUrl not configured. Run docdex setup or set MCODA_DOCDEX_URL.");
119
+ }
120
+ await this.ensureRepoInitialized(baseUrl);
121
+ const url = new URL(pathname, baseUrl);
122
+ const buildHeaders = () => {
123
+ const headers = { "Content-Type": "application/json" };
124
+ if (this.options.authToken)
125
+ headers.authorization = `Bearer ${this.options.authToken}`;
126
+ if (this.repoId)
127
+ headers["x-docdex-repo-id"] = this.repoId;
128
+ return { ...headers, ...init?.headers };
129
+ };
130
+ const response = await fetch(url, { ...init, headers: buildHeaders() });
79
131
  if (!response.ok) {
80
- throw new Error(`Docdex request failed (${response.status}): ${await response.text()}`);
132
+ const message = await response.text();
133
+ if (message.includes("missing_repo")) {
134
+ await this.ensureRepoInitialized(baseUrl, true);
135
+ if (this.repoId) {
136
+ const retry = await fetch(url, { ...init, headers: buildHeaders() });
137
+ if (retry.ok)
138
+ return retry;
139
+ const retryMessage = await retry.text();
140
+ throw new Error(`Docdex request failed (${retry.status}): ${retryMessage}`);
141
+ }
142
+ }
143
+ throw new Error(`Docdex request failed (${response.status}): ${message}`);
81
144
  }
82
- return (await response.json());
145
+ return response;
83
146
  }
84
- async saveStore(store) {
85
- const storePath = this.getStorePath();
86
- await fs.mkdir(path.dirname(storePath), { recursive: true });
87
- const payload = { ...store, updatedAt: nowIso() };
88
- await fs.writeFile(storePath, JSON.stringify(payload, null, 2), "utf8");
147
+ buildLocalDoc(docType, docPath, content, metadata) {
148
+ const now = nowIso();
149
+ const id = `local-${randomUUID()}`;
150
+ return {
151
+ id,
152
+ docType,
153
+ path: docPath,
154
+ title: docPath ? path.basename(docPath) : undefined,
155
+ content,
156
+ metadata,
157
+ createdAt: now,
158
+ updatedAt: now,
159
+ segments: segmentize(id, content),
160
+ };
161
+ }
162
+ coerceSearchResults(raw, fallbackDocType) {
163
+ const items = Array.isArray(raw)
164
+ ? raw
165
+ : Array.isArray(raw?.results)
166
+ ? raw.results
167
+ : Array.isArray(raw?.hits)
168
+ ? raw.hits
169
+ : [];
170
+ const now = nowIso();
171
+ return items
172
+ .map((item, idx) => {
173
+ if (!item || typeof item !== "object")
174
+ return undefined;
175
+ const id = (item.doc_id ?? item.docId ?? item.id ?? `doc-${idx + 1}`);
176
+ const pathValue = (item.path ?? item.file ?? item.rel_path ?? item.file_path);
177
+ const title = (item.title ?? item.name ?? item.file_name);
178
+ const docType = (item.doc_type ?? item.docType ?? item.type ?? inferDocType(pathValue, fallbackDocType ?? "DOC"));
179
+ const snippet = (item.snippet ?? item.summary ?? item.excerpt);
180
+ const content = (item.content ?? snippet);
181
+ const segments = Array.isArray(item.segments)
182
+ ? item.segments.map((seg, segIdx) => ({
183
+ id: seg.id ?? `${id}-seg-${segIdx + 1}`,
184
+ docId: id,
185
+ index: segIdx,
186
+ content: seg.content ?? seg.text ?? "",
187
+ heading: seg.heading ?? seg.title ?? undefined,
188
+ }))
189
+ : snippet
190
+ ? [
191
+ {
192
+ id: `${id}-seg-1`,
193
+ docId: id,
194
+ index: 0,
195
+ content: snippet,
196
+ heading: undefined,
197
+ },
198
+ ]
199
+ : undefined;
200
+ return {
201
+ id,
202
+ docType,
203
+ path: pathValue,
204
+ title,
205
+ content,
206
+ createdAt: item.created_at ?? item.createdAt ?? now,
207
+ updatedAt: item.updated_at ?? item.updatedAt ?? now,
208
+ segments,
209
+ };
210
+ })
211
+ .filter(Boolean);
89
212
  }
90
213
  async fetchDocumentById(id) {
91
- if (this.options.baseUrl) {
214
+ const response = await this.fetchRemote(`/snippet/${encodeURIComponent(id)}?text_only=true`);
215
+ const contentType = response.headers.get("content-type") ?? "";
216
+ const body = await response.text();
217
+ let content = body;
218
+ if (contentType.includes("application/json")) {
92
219
  try {
93
- const doc = await this.fetchRemote(`/documents/${id}`);
94
- return doc;
220
+ const parsed = JSON.parse(body);
221
+ content =
222
+ parsed.text ??
223
+ parsed.content ??
224
+ parsed.snippet ??
225
+ body;
95
226
  }
96
- catch (error) {
97
- // fall through to local if remote fails
98
- // eslint-disable-next-line no-console
99
- console.warn(`Docdex remote fetch failed, falling back to local: ${error.message}`);
227
+ catch {
228
+ content = body;
100
229
  }
101
230
  }
102
- const store = await this.loadStore();
103
- const doc = store.documents.find((d) => d.id === id);
104
- if (!doc) {
105
- throw new Error(`Docdex document not found: ${id}`);
106
- }
107
- const segments = store.segments.filter((s) => s.docId === id);
108
- return { ...doc, segments };
231
+ const now = nowIso();
232
+ return {
233
+ id,
234
+ docType: "DOC",
235
+ content,
236
+ createdAt: now,
237
+ updatedAt: now,
238
+ segments: content ? segmentize(id, content) : undefined,
239
+ };
109
240
  }
110
241
  async findDocumentByPath(docPath, docType) {
111
242
  const normalized = this.normalizePath(docPath);
112
- const store = await this.loadStore();
113
- const doc = store.documents.find((d) => d.path === normalized && (!docType || d.docType.toLowerCase() === docType.toLowerCase()));
114
- if (!doc)
243
+ const query = normalized ?? docPath;
244
+ const docs = await this.search({ query, docType });
245
+ if (!docs.length)
115
246
  return undefined;
116
- return { ...doc, segments: store.segments.filter((s) => s.docId === doc.id) };
247
+ if (!normalized)
248
+ return docs[0];
249
+ return docs.find((doc) => doc.path === normalized) ?? docs[0];
117
250
  }
118
251
  async search(filter) {
119
- if (this.options.baseUrl) {
120
- try {
121
- const params = new URLSearchParams();
122
- if (filter.docType)
123
- params.set("doc_type", filter.docType);
124
- if (filter.projectKey)
125
- params.set("project_key", filter.projectKey);
126
- if (filter.query)
127
- params.set("q", filter.query);
128
- if (filter.profile)
129
- params.set("profile", filter.profile);
130
- const path = `/documents?${params.toString()}`;
131
- const docs = await this.fetchRemote(path);
132
- return docs;
133
- }
134
- catch (error) {
135
- // eslint-disable-next-line no-console
136
- console.warn(`Docdex remote search failed, falling back to local: ${error.message}`);
137
- }
252
+ const params = new URLSearchParams();
253
+ const queryParts = [filter.query, filter.docType, filter.projectKey].filter(Boolean);
254
+ const query = queryParts.join(" ").trim();
255
+ if (query)
256
+ params.set("q", query);
257
+ if (filter.profile)
258
+ params.set("profile", filter.profile);
259
+ if (filter.docType)
260
+ params.set("doc_type", filter.docType);
261
+ if (filter.projectKey)
262
+ params.set("project_key", filter.projectKey);
263
+ params.set("limit", "8");
264
+ const baseUrl = await this.resolveBaseUrl();
265
+ if (!baseUrl) {
266
+ return [];
138
267
  }
139
- const store = await this.loadStore();
140
- return store.documents
141
- .filter((doc) => {
142
- if (filter.docType && doc.docType.toLowerCase() !== filter.docType.toLowerCase())
143
- return false;
144
- if (filter.projectKey && doc.metadata && doc.metadata.projectKey !== filter.projectKey)
145
- return false;
146
- return true;
147
- })
148
- .map((doc) => ({ ...doc, segments: store.segments.filter((s) => s.docId === doc.id) }));
268
+ const response = await this.fetchRemote(`/search?${params.toString()}`);
269
+ const payload = (await response.json());
270
+ return this.coerceSearchResults(payload, filter.docType);
271
+ }
272
+ async reindex() {
273
+ const repoRoot = this.options.workspaceRoot ?? process.cwd();
274
+ const baseUrl = await this.resolveBaseUrl();
275
+ await runDocdex(["index", "--repo", repoRoot], {
276
+ cwd: repoRoot,
277
+ env: baseUrl
278
+ ? {
279
+ DOCDEX_URL: baseUrl,
280
+ MCODA_DOCDEX_URL: baseUrl,
281
+ }
282
+ : undefined,
283
+ });
149
284
  }
150
285
  async registerDocument(input) {
151
- if (this.options.baseUrl) {
152
- try {
153
- const registered = await this.fetchRemote(`/documents`, {
154
- method: "POST",
155
- body: JSON.stringify({
156
- doc_type: input.docType,
157
- path: input.path,
158
- title: input.title,
159
- content: input.content,
160
- metadata: input.metadata,
161
- }),
162
- });
163
- return registered;
164
- }
165
- catch (error) {
166
- // eslint-disable-next-line no-console
167
- console.warn(`Docdex remote register failed, falling back to local: ${error.message}`);
168
- }
286
+ const baseUrl = await this.resolveBaseUrl();
287
+ if (!baseUrl) {
288
+ const normalized = input.path ? this.normalizePath(input.path) ?? input.path : undefined;
289
+ return this.buildLocalDoc(input.docType, normalized, input.content, input.metadata);
169
290
  }
170
- const store = await this.loadStore();
171
- const normalizedPath = this.normalizePath(input.path);
172
- const existingByPath = normalizedPath
173
- ? store.documents.find((d) => d.path === normalizedPath &&
174
- d.docType.toLowerCase() === input.docType.toLowerCase() &&
175
- (input.metadata?.projectKey ? d.metadata?.projectKey === input.metadata.projectKey : true))
176
- : undefined;
177
- const now = nowIso();
178
- if (existingByPath) {
179
- const updated = {
180
- ...existingByPath,
181
- content: input.content ?? existingByPath.content,
182
- metadata: { ...(existingByPath.metadata ?? {}), ...(input.metadata ?? {}) },
183
- title: input.title ?? existingByPath.title,
184
- updatedAt: now,
185
- };
186
- const segments = segmentize(updated.id, input.content);
187
- store.documents[store.documents.findIndex((d) => d.id === existingByPath.id)] = updated;
188
- store.segments = store.segments.filter((s) => s.docId !== updated.id).concat(segments);
189
- await this.saveStore(store);
190
- return { ...updated, segments };
191
- }
192
- const doc = {
193
- id: randomUUID(),
194
- docType: input.docType,
195
- path: normalizedPath,
196
- content: input.content,
197
- metadata: input.metadata,
198
- title: input.title,
199
- createdAt: now,
200
- updatedAt: now,
201
- };
202
- const segments = segmentize(doc.id, input.content);
203
- store.documents.push(doc);
204
- store.segments.push(...segments);
205
- await this.saveStore(store);
206
- return { ...doc, segments };
291
+ if (!input.path) {
292
+ throw new Error("Docdex register requires a file path to ingest.");
293
+ }
294
+ await this.ensureRepoInitialized(baseUrl);
295
+ const resolvedPath = path.isAbsolute(input.path)
296
+ ? input.path
297
+ : path.join(this.options.workspaceRoot ?? process.cwd(), input.path);
298
+ const repoRoot = this.options.workspaceRoot ?? process.cwd();
299
+ await runDocdex(["ingest", "--repo", repoRoot, "--file", resolvedPath], {
300
+ cwd: repoRoot,
301
+ env: {
302
+ DOCDEX_URL: baseUrl,
303
+ MCODA_DOCDEX_URL: baseUrl,
304
+ },
305
+ });
306
+ const registered = await this.findDocumentByPath(resolvedPath, input.docType).catch(() => undefined);
307
+ if (registered)
308
+ return registered;
309
+ return this.buildLocalDoc(input.docType, resolvedPath, input.content, input.metadata);
207
310
  }
208
311
  async ensureRegisteredFromFile(docPath, docType, metadata) {
209
312
  const normalizedPath = this.normalizePath(docPath) ?? docPath;
210
- const existing = await this.findDocumentByPath(normalizedPath, docType);
211
- if (existing)
212
- return existing;
313
+ try {
314
+ const existing = await this.findDocumentByPath(normalizedPath, docType);
315
+ if (existing)
316
+ return existing;
317
+ }
318
+ catch {
319
+ // ignore docdex lookup failures; fall back to local
320
+ }
213
321
  const content = await fs.readFile(docPath, "utf8");
214
- return this.registerDocument({ docType, path: docPath, content, metadata });
322
+ const inferredType = docType || inferDocType(docPath);
323
+ const baseUrl = await this.resolveBaseUrl();
324
+ if (!baseUrl) {
325
+ return this.buildLocalDoc(inferredType, normalizedPath, content, metadata);
326
+ }
327
+ try {
328
+ return await this.registerDocument({ docType: inferredType, path: docPath, content, metadata });
329
+ }
330
+ catch {
331
+ return this.buildLocalDoc(inferredType, normalizedPath, content, metadata);
332
+ }
215
333
  }
216
334
  }
@@ -0,0 +1,42 @@
1
+ export interface DocdexCheckResult {
2
+ status?: string;
3
+ success?: boolean;
4
+ checks?: Array<{
5
+ name?: string;
6
+ status?: string;
7
+ message?: string;
8
+ details?: Record<string, unknown>;
9
+ }>;
10
+ }
11
+ export interface DocdexBrowserInfo {
12
+ ok: boolean;
13
+ message?: string;
14
+ browsersPath?: string;
15
+ browsers?: Array<{
16
+ name?: string;
17
+ path?: string;
18
+ version?: string;
19
+ }>;
20
+ }
21
+ export declare const resolveDocdexBinary: () => string | undefined;
22
+ export declare const resolvePlaywrightCli: () => string | undefined;
23
+ export declare const runDocdex: (args: string[], options?: {
24
+ cwd?: string;
25
+ env?: NodeJS.ProcessEnv;
26
+ }) => Promise<{
27
+ stdout: string;
28
+ stderr: string;
29
+ }>;
30
+ export declare const readDocdexCheck: (options?: {
31
+ cwd?: string;
32
+ env?: NodeJS.ProcessEnv;
33
+ }) => Promise<DocdexCheckResult>;
34
+ export declare const resolveDocdexBaseUrl: (options?: {
35
+ cwd?: string;
36
+ env?: NodeJS.ProcessEnv;
37
+ }) => Promise<string | undefined>;
38
+ export declare const resolveDocdexBrowserInfo: (options?: {
39
+ cwd?: string;
40
+ env?: NodeJS.ProcessEnv;
41
+ }) => Promise<DocdexBrowserInfo>;
42
+ //# sourceMappingURL=DocdexRuntime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocdexRuntime.d.ts","sourceRoot":"","sources":["../../src/docdex/DocdexRuntime.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,KAAK,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACnC,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACtE;AAuBD,eAAO,MAAM,mBAAmB,QAAO,MAAM,GAAG,SAI/C,CAAC;AAEF,eAAO,MAAM,oBAAoB,QAAO,MAAM,GAAG,SAehD,CAAC;AAEF,eAAO,MAAM,SAAS,GACpB,MAAM,MAAM,EAAE,EACd,UAAS;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAO,KACtD,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAU5C,CAAC;AAEF,eAAO,MAAM,eAAe,GAAU,UAAS;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAO,KAAG,OAAO,CAAC,iBAAiB,CAsBxH,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAU,UAAS;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAO,KAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAsB9H,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,UAAS;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAO,KACtD,OAAO,CAAC,iBAAiB,CAgC3B,CAAC"}
@@ -0,0 +1,145 @@
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+ import { createRequire } from "node:module";
4
+ import { execFile as execFileCb } from "node:child_process";
5
+ import { promisify } from "node:util";
6
+ const execFile = promisify(execFileCb);
7
+ const DOCDEX_ENV_URLS = ["MCODA_DOCDEX_URL", "DOCDEX_URL"];
8
+ const DEFAULT_DOCDEX_STATE_DIR = path.join(os.homedir(), ".docdex", "state");
9
+ const buildDocdexEnv = (env) => {
10
+ const merged = { ...process.env, ...(env ?? {}) };
11
+ if (!merged.DOCDEX_STATE_DIR) {
12
+ merged.DOCDEX_STATE_DIR = DEFAULT_DOCDEX_STATE_DIR;
13
+ }
14
+ return merged;
15
+ };
16
+ const resolveDocdexPackageRoot = () => {
17
+ try {
18
+ const require = createRequire(import.meta.url);
19
+ const pkgPath = require.resolve("docdex/package.json");
20
+ return path.dirname(pkgPath);
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ };
26
+ export const resolveDocdexBinary = () => {
27
+ const root = resolveDocdexPackageRoot();
28
+ if (!root)
29
+ return undefined;
30
+ return path.join(root, "bin", "docdex.js");
31
+ };
32
+ export const resolvePlaywrightCli = () => {
33
+ const require = createRequire(import.meta.url);
34
+ try {
35
+ return require.resolve("playwright/cli.js");
36
+ }
37
+ catch {
38
+ // fall through to docdex-local resolution
39
+ }
40
+ const root = resolveDocdexPackageRoot();
41
+ if (!root)
42
+ return undefined;
43
+ try {
44
+ const requireFromDocdex = createRequire(path.join(root, "package.json"));
45
+ return requireFromDocdex.resolve("playwright/cli.js");
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ };
51
+ export const runDocdex = async (args, options = {}) => {
52
+ const binary = resolveDocdexBinary();
53
+ if (!binary) {
54
+ throw new Error("Docdex npm package not found. Install docdex and retry.");
55
+ }
56
+ const { stdout, stderr } = await execFile(process.execPath, [binary, ...args], {
57
+ cwd: options.cwd,
58
+ env: buildDocdexEnv(options.env),
59
+ });
60
+ return { stdout: stdout ?? "", stderr: stderr ?? "" };
61
+ };
62
+ export const readDocdexCheck = async (options = {}) => {
63
+ let stdout = "";
64
+ let stderr = "";
65
+ try {
66
+ ({ stdout, stderr } = await runDocdex(["check"], options));
67
+ }
68
+ catch (error) {
69
+ const execError = error;
70
+ stdout = typeof execError.stdout === "string" ? execError.stdout : execError.stdout?.toString() ?? "";
71
+ stderr = typeof execError.stderr === "string" ? execError.stderr : execError.stderr?.toString() ?? "";
72
+ if (!stdout && !stderr) {
73
+ throw error;
74
+ }
75
+ }
76
+ const trimmed = stdout.trim() || stderr.trim();
77
+ if (!trimmed) {
78
+ throw new Error("Docdex check returned empty output");
79
+ }
80
+ try {
81
+ return JSON.parse(trimmed);
82
+ }
83
+ catch (error) {
84
+ throw new Error(`Docdex check returned invalid JSON: ${error.message}`);
85
+ }
86
+ };
87
+ export const resolveDocdexBaseUrl = async (options = {}) => {
88
+ for (const key of DOCDEX_ENV_URLS) {
89
+ const envValue = process.env[key];
90
+ if (envValue)
91
+ return envValue;
92
+ }
93
+ if (process.env.MCODA_SKIP_DOCDEX_CHECKS === "1" ||
94
+ process.env.MCODA_SKIP_DOCDEX_RUNTIME_CHECKS === "1" ||
95
+ (process.platform === "win32" && process.env.CI)) {
96
+ return undefined;
97
+ }
98
+ try {
99
+ const check = await readDocdexCheck(options);
100
+ const bind = check.checks?.find((c) => c.name === "bind")?.details;
101
+ const bindAddr = bind?.bind_addr;
102
+ if (!bindAddr)
103
+ return undefined;
104
+ if (bindAddr.startsWith("http://") || bindAddr.startsWith("https://"))
105
+ return bindAddr;
106
+ return `http://${bindAddr}`;
107
+ }
108
+ catch {
109
+ return undefined;
110
+ }
111
+ };
112
+ export const resolveDocdexBrowserInfo = async (options = {}) => {
113
+ const setupHint = "Run `docdex setup` to install Playwright and at least one browser.";
114
+ try {
115
+ const check = await readDocdexCheck(options);
116
+ const browserCheck = check.checks?.find((c) => c.name === "browser");
117
+ if (!browserCheck || browserCheck.status !== "ok") {
118
+ return {
119
+ ok: false,
120
+ message: `${browserCheck?.message ?? "Docdex browser check failed."} ${setupHint}`,
121
+ };
122
+ }
123
+ const details = browserCheck.details ?? {};
124
+ const playwright = details.playwright ?? {};
125
+ const browsers = Array.isArray(playwright.browsers) ? playwright.browsers : [];
126
+ if (!browsers.length) {
127
+ return {
128
+ ok: false,
129
+ message: `Docdex has no Playwright browsers configured. ${setupHint}`,
130
+ };
131
+ }
132
+ const browsersPath = typeof playwright.browsers_path === "string" ? playwright.browsers_path : undefined;
133
+ return {
134
+ ok: true,
135
+ browsersPath,
136
+ browsers,
137
+ };
138
+ }
139
+ catch (error) {
140
+ return {
141
+ ok: false,
142
+ message: `Docdex check failed: ${error.message}. ${setupHint}`,
143
+ };
144
+ }
145
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"ChromiumQaAdapter.d.ts","sourceRoot":"","sources":["../../src/qa/ChromiumQaAdapter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAOtE,qBAAa,iBAAkB,YAAW,SAAS;IACjD,OAAO,CAAC,UAAU;IASZ,eAAe,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,cAAc,CAAC;YAiBpE,WAAW;IAYnB,MAAM,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;CAqCvE"}
1
+ {"version":3,"file":"ChromiumQaAdapter.d.ts","sourceRoot":"","sources":["../../src/qa/ChromiumQaAdapter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAQtE,qBAAa,iBAAkB,YAAW,SAAS;IACjD,OAAO,CAAC,UAAU;IASZ,eAAe,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,cAAc,CAAC;YAqBpE,WAAW;IAYnB,MAAM,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;CA8CvE"}
@@ -2,6 +2,7 @@ import path from 'node:path';
2
2
  import { exec as execCb } from 'node:child_process';
3
3
  import { promisify } from 'node:util';
4
4
  import fs from 'node:fs/promises';
5
+ import { resolveDocdexBrowserInfo, resolvePlaywrightCli } from '../docdex/DocdexRuntime.js';
5
6
  const exec = promisify(execCb);
6
7
  const shouldSkipInstall = (ctx) => process.env.MCODA_QA_SKIP_INSTALL === '1' || ctx.env?.MCODA_QA_SKIP_INSTALL === '1';
7
8
  export class ChromiumQaAdapter {
@@ -17,20 +18,21 @@ export class ChromiumQaAdapter {
17
18
  if (shouldSkipInstall(ctx))
18
19
  return { ok: true, details: { skipped: true } };
19
20
  const cwd = this.resolveCwd(profile, ctx);
20
- try {
21
- await exec('npx playwright --version', { cwd, env: { ...process.env, ...profile.env, ...ctx.env } });
22
- return { ok: true };
23
- }
24
- catch (versionError) {
25
- const installCommand = profile.install_command ?? 'npx playwright install chromium';
26
- try {
27
- await exec(installCommand, { cwd, env: { ...process.env, ...profile.env, ...ctx.env } });
28
- return { ok: true, details: { installedVia: installCommand } };
29
- }
30
- catch (error) {
31
- return { ok: false, message: error?.message ?? versionError?.message ?? 'Chromium QA install failed' };
32
- }
21
+ const browserInfo = await resolveDocdexBrowserInfo({ cwd });
22
+ if (!browserInfo.ok) {
23
+ return {
24
+ ok: false,
25
+ message: browserInfo.message ??
26
+ 'Playwright browsers not installed. Run `docdex setup` and install Playwright with at least one browser.',
27
+ };
33
28
  }
29
+ return {
30
+ ok: true,
31
+ details: {
32
+ playwrightBrowsersPath: browserInfo.browsersPath,
33
+ browsers: browserInfo.browsers,
34
+ },
35
+ };
34
36
  }
35
37
  async persistLogs(ctx, stdout, stderr) {
36
38
  const artifacts = [];
@@ -45,13 +47,22 @@ export class ChromiumQaAdapter {
45
47
  return artifacts;
46
48
  }
47
49
  async invoke(profile, ctx) {
48
- const command = ctx.testCommandOverride ?? profile.test_command ?? 'npx playwright test --reporter=list';
50
+ const playwrightCli = resolvePlaywrightCli();
51
+ const defaultCommand = playwrightCli
52
+ ? `node ${playwrightCli} test --reporter=list`
53
+ : 'npx playwright test --reporter=list';
54
+ const command = ctx.testCommandOverride ?? profile.test_command ?? defaultCommand;
49
55
  const startedAt = new Date().toISOString();
50
56
  const cwd = this.resolveCwd(profile, ctx);
51
57
  try {
52
58
  const { stdout, stderr } = await exec(command, {
53
59
  cwd,
54
- env: { ...process.env, ...profile.env, ...ctx.env },
60
+ env: {
61
+ ...process.env,
62
+ ...profile.env,
63
+ ...ctx.env,
64
+ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD ?? '1',
65
+ },
55
66
  });
56
67
  const finishedAt = new Date().toISOString();
57
68
  const artifacts = await this.persistLogs(ctx, stdout, stderr);
@@ -17,12 +17,13 @@ export declare class VcsClient {
17
17
  noGpgSign?: boolean;
18
18
  }): Promise<void>;
19
19
  merge(cwd: string, source: string, target: string, ensureClean?: boolean): Promise<void>;
20
+ abortMerge(cwd: string): Promise<void>;
20
21
  push(cwd: string, remote: string, branch: string): Promise<void>;
21
22
  pull(cwd: string, remote: string, branch: string, ffOnly?: boolean): Promise<void>;
22
23
  status(cwd: string): Promise<string>;
23
24
  dirtyPaths(cwd: string): Promise<string[]>;
24
25
  conflictPaths(cwd: string): Promise<string[]>;
25
- ensureClean(cwd: string, ignoreDotMcoda?: boolean): Promise<void>;
26
+ ensureClean(cwd: string, ignoreDotMcoda?: boolean, ignorePaths?: string[]): Promise<void>;
26
27
  lastCommitSha(cwd: string): Promise<string>;
27
28
  diff(cwd: string, base: string, head: string, paths?: string[]): Promise<string>;
28
29
  }
@@ -1 +1 @@
1
- {"version":3,"file":"VcsClient.d.ts","sourceRoot":"","sources":["../../src/vcs/VcsClient.ts"],"names":[],"mappings":"AAQA,qBAAa,SAAS;YACN,MAAM;YAKN,YAAY;IASpB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASxC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IASlD,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;YASnD,UAAU;YASV,gBAAgB;IAaxB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY1D,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1D,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQhF,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBrD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,MAAM,CACV,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAO,GACxD,OAAO,CAAC,IAAI,CAAC;IAOV,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAQtF,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKpC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAsB1C,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAS7C,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,UAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ9D,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK3C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;CAQvF"}
1
+ {"version":3,"file":"VcsClient.d.ts","sourceRoot":"","sources":["../../src/vcs/VcsClient.ts"],"names":[],"mappings":"AAQA,qBAAa,SAAS;YACN,MAAM;YAKN,YAAY;IASpB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASxC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IASlD,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;YASnD,UAAU;YASV,gBAAgB;IAaxB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY1D,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1D,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQhF,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBrD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,MAAM,CACV,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAO,GACxD,OAAO,CAAC,IAAI,CAAC;IAOV,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAQtF,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQtC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKpC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAsB1C,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAS7C,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,UAAO,EAAE,WAAW,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAW1F,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK3C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;CAQvF"}
@@ -103,14 +103,22 @@ export class VcsClient {
103
103
  return;
104
104
  }
105
105
  catch (error) {
106
- // If the patch is already applied, a reverse --check succeeds; treat that as a no-op.
107
- const reverseCheckCmd = `cat <<'__PATCH__' | git apply --reverse --check --whitespace=nowarn\n${patch}\n__PATCH__`;
106
+ // Retry with 3-way merge for drifted files.
107
+ const apply3wayCmd = `cat <<'__PATCH__' | git apply --3way --whitespace=nowarn\n${patch}\n__PATCH__`;
108
108
  try {
109
- await exec(reverseCheckCmd, opts);
109
+ await exec(apply3wayCmd, opts);
110
110
  return;
111
111
  }
112
112
  catch {
113
- throw error;
113
+ // If the patch is already applied, a reverse --check succeeds; treat that as a no-op.
114
+ const reverseCheckCmd = `cat <<'__PATCH__' | git apply --reverse --check --whitespace=nowarn\n${patch}\n__PATCH__`;
115
+ try {
116
+ await exec(reverseCheckCmd, opts);
117
+ return;
118
+ }
119
+ catch {
120
+ throw error;
121
+ }
114
122
  }
115
123
  }
116
124
  }
@@ -132,6 +140,14 @@ export class VcsClient {
132
140
  }
133
141
  await this.runGit(cwd, ["merge", "--no-edit", source]);
134
142
  }
143
+ async abortMerge(cwd) {
144
+ try {
145
+ await this.runGit(cwd, ["merge", "--abort"]);
146
+ }
147
+ catch {
148
+ // Ignore when no merge is in progress.
149
+ }
150
+ }
135
151
  async push(cwd, remote, branch) {
136
152
  await this.runGit(cwd, ["push", remote, branch]);
137
153
  }
@@ -179,9 +195,13 @@ export class VcsClient {
179
195
  return [];
180
196
  }
181
197
  }
182
- async ensureClean(cwd, ignoreDotMcoda = true) {
198
+ async ensureClean(cwd, ignoreDotMcoda = true, ignorePaths = []) {
183
199
  const dirty = await this.dirtyPaths(cwd);
184
- const filtered = ignoreDotMcoda ? dirty.filter((p) => !p.startsWith(".mcoda")) : dirty;
200
+ const filtered = dirty.filter((p) => {
201
+ if (ignoreDotMcoda && p.startsWith(".mcoda"))
202
+ return false;
203
+ return !ignorePaths.some((prefix) => p.startsWith(prefix));
204
+ });
185
205
  if (filtered.length) {
186
206
  throw new Error(`Working tree dirty: ${filtered.join(", ")}`);
187
207
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcoda/integrations",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "External integrations for mcoda (vcs, QA, telemetry).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,7 +37,8 @@
37
37
  "access": "public"
38
38
  },
39
39
  "dependencies": {
40
- "@mcoda/shared": "0.1.8"
40
+ "docdex": "^0.2.22",
41
+ "@mcoda/shared": "0.1.9"
41
42
  },
42
43
  "scripts": {
43
44
  "build": "tsc -p tsconfig.json",