@fabricorg/cli 0.7.0 → 0.7.1
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/dist/fabric.js +1946 -637
- package/dist/fabric.js.map +1 -1
- package/dist/packages/cli/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
package/dist/fabric.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Command as Command23 } from "commander";
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "@fabricorg/cli",
|
|
9
|
-
version: "0.7.
|
|
9
|
+
version: "0.7.1",
|
|
10
10
|
description: "CLI for the Fabric AI platform",
|
|
11
11
|
keywords: [
|
|
12
12
|
"fabric",
|
|
@@ -135,6 +135,57 @@ var DEFAULT_RETRY = {
|
|
|
135
135
|
};
|
|
136
136
|
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
137
137
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PATCH", "PUT", "DELETE"]);
|
|
138
|
+
var RateLimitDelayError = class extends FabricError {
|
|
139
|
+
constructor(message, delayMs) {
|
|
140
|
+
super(message, 429);
|
|
141
|
+
this.delayMs = delayMs;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
function retryAfterMs(value) {
|
|
145
|
+
if (value === null) {
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
if (/^\d+$/.test(value)) {
|
|
149
|
+
return Number(value) * 1e3;
|
|
150
|
+
}
|
|
151
|
+
const date = Date.parse(value);
|
|
152
|
+
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : void 0;
|
|
153
|
+
}
|
|
154
|
+
async function boundedJson(res, maxBytes) {
|
|
155
|
+
const reader = res.body?.getReader();
|
|
156
|
+
if (!reader) {
|
|
157
|
+
throw new FabricError("Empty repository response", res.status);
|
|
158
|
+
}
|
|
159
|
+
const chunks = [];
|
|
160
|
+
let length = 0;
|
|
161
|
+
try {
|
|
162
|
+
for (; ; ) {
|
|
163
|
+
const next = await reader.read();
|
|
164
|
+
if (next.done) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
length += next.value.byteLength;
|
|
168
|
+
if (length > maxBytes) {
|
|
169
|
+
await reader.cancel();
|
|
170
|
+
throw new FabricError(
|
|
171
|
+
"Repository response is too large",
|
|
172
|
+
res.status,
|
|
173
|
+
"RESPONSE_TOO_LARGE"
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
chunks.push(next.value);
|
|
177
|
+
}
|
|
178
|
+
} finally {
|
|
179
|
+
reader.releaseLock();
|
|
180
|
+
}
|
|
181
|
+
const bytes = new Uint8Array(length);
|
|
182
|
+
let offset = 0;
|
|
183
|
+
for (const chunk of chunks) {
|
|
184
|
+
bytes.set(chunk, offset);
|
|
185
|
+
offset += chunk.byteLength;
|
|
186
|
+
}
|
|
187
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
188
|
+
}
|
|
138
189
|
function env(name) {
|
|
139
190
|
if (typeof process === "undefined" || !process.env) {
|
|
140
191
|
return void 0;
|
|
@@ -235,10 +286,10 @@ var FabricHttpClient = class _FabricHttpClient {
|
|
|
235
286
|
* Inject default `org` / `personal` query params if the path doesn't already
|
|
236
287
|
* carry one. Resources that explicitly pass `?org=other` are untouched.
|
|
237
288
|
*/
|
|
238
|
-
buildUrl(
|
|
239
|
-
const url = `${this.baseUrl}/api/v1${
|
|
240
|
-
const hasOrg = /[?&]org=/.test(
|
|
241
|
-
const hasPersonal = /[?&]personal=/.test(
|
|
289
|
+
buildUrl(path42) {
|
|
290
|
+
const url = `${this.baseUrl}/api/v1${path42}`;
|
|
291
|
+
const hasOrg = /[?&]org=/.test(path42);
|
|
292
|
+
const hasPersonal = /[?&]personal=/.test(path42);
|
|
242
293
|
if (hasOrg || hasPersonal) {
|
|
243
294
|
return url;
|
|
244
295
|
}
|
|
@@ -251,21 +302,22 @@ var FabricHttpClient = class _FabricHttpClient {
|
|
|
251
302
|
if (additions.length === 0) {
|
|
252
303
|
return url;
|
|
253
304
|
}
|
|
254
|
-
const sep =
|
|
305
|
+
const sep = path42.includes("?") ? "&" : "?";
|
|
255
306
|
return `${url}${sep}${additions.join("&")}`;
|
|
256
307
|
}
|
|
257
|
-
async request(method,
|
|
258
|
-
const url = this.buildUrl(
|
|
308
|
+
async request(method, path42, options = {}) {
|
|
309
|
+
const url = this.buildUrl(path42);
|
|
259
310
|
const isMutating = MUTATING_METHODS.has(method);
|
|
260
311
|
const idempotencyKey = options.idempotencyKey ?? (isMutating ? randomId() : void 0);
|
|
261
|
-
const
|
|
312
|
+
const retries = (options.retry ?? this.retry).maxRetries;
|
|
313
|
+
const maxAttempts = (options.boundedRead ? Math.min(retries, 2) : retries) + 1;
|
|
262
314
|
let lastError;
|
|
263
315
|
for (let attempt2 = 1; attempt2 <= maxAttempts; attempt2++) {
|
|
264
316
|
if (options.signal?.aborted) {
|
|
265
317
|
throw options.signal.reason;
|
|
266
318
|
}
|
|
267
319
|
const start = Date.now();
|
|
268
|
-
this.emit({ kind: "request", method, path:
|
|
320
|
+
this.emit({ kind: "request", method, path: path42, attempt: attempt2 });
|
|
269
321
|
try {
|
|
270
322
|
const result = await this.executeOnce(
|
|
271
323
|
method,
|
|
@@ -276,7 +328,7 @@ var FabricHttpClient = class _FabricHttpClient {
|
|
|
276
328
|
this.emit({
|
|
277
329
|
kind: "response",
|
|
278
330
|
method,
|
|
279
|
-
path:
|
|
331
|
+
path: path42,
|
|
280
332
|
attempt: attempt2,
|
|
281
333
|
status: 200,
|
|
282
334
|
durationMs: Date.now() - start
|
|
@@ -285,11 +337,11 @@ var FabricHttpClient = class _FabricHttpClient {
|
|
|
285
337
|
} catch (err) {
|
|
286
338
|
lastError = err;
|
|
287
339
|
const status2 = err instanceof FabricError ? err.status : void 0;
|
|
288
|
-
const retriable = attempt2 < maxAttempts && this.shouldRetry(method, err);
|
|
340
|
+
const retriable = attempt2 < maxAttempts && this.shouldRetry(method, err) && !(err instanceof RateLimitDelayError && err.delayMs > 6e4);
|
|
289
341
|
this.emit({
|
|
290
342
|
kind: retriable ? "retry" : "error",
|
|
291
343
|
method,
|
|
292
|
-
path:
|
|
344
|
+
path: path42,
|
|
293
345
|
attempt: attempt2,
|
|
294
346
|
status: status2,
|
|
295
347
|
durationMs: Date.now() - start,
|
|
@@ -301,7 +353,7 @@ var FabricHttpClient = class _FabricHttpClient {
|
|
|
301
353
|
if (!retriable) {
|
|
302
354
|
throw err;
|
|
303
355
|
}
|
|
304
|
-
const delay2 = Math.min(
|
|
356
|
+
const delay2 = err instanceof RateLimitDelayError ? err.delayMs : Math.min(
|
|
305
357
|
this.retry.initialDelayMs * this.retry.multiplier ** (attempt2 - 1),
|
|
306
358
|
this.retry.maxDelayMs
|
|
307
359
|
);
|
|
@@ -354,22 +406,51 @@ var FabricHttpClient = class _FabricHttpClient {
|
|
|
354
406
|
"NETWORK_ERROR"
|
|
355
407
|
);
|
|
356
408
|
}
|
|
357
|
-
|
|
409
|
+
if (!options.boundedRead) {
|
|
410
|
+
clearTimeout(timeoutHandle);
|
|
411
|
+
}
|
|
358
412
|
const upgradeNotice = res.headers?.get("X-Fabric-Cli-Upgrade");
|
|
359
413
|
if (upgradeNotice) {
|
|
360
414
|
this.notifyUpgrade(upgradeNotice);
|
|
361
415
|
}
|
|
416
|
+
if (options.boundedRead && res.status === 429) {
|
|
417
|
+
try {
|
|
418
|
+
await res.body?.cancel();
|
|
419
|
+
} finally {
|
|
420
|
+
clearTimeout(timeoutHandle);
|
|
421
|
+
}
|
|
422
|
+
options.signal?.throwIfAborted();
|
|
423
|
+
throw new RateLimitDelayError(
|
|
424
|
+
"Too many requests. Wait a minute and try again.",
|
|
425
|
+
Math.max(
|
|
426
|
+
1e3,
|
|
427
|
+
retryAfterMs(res.headers.get("Retry-After")) ?? 6e4
|
|
428
|
+
)
|
|
429
|
+
);
|
|
430
|
+
}
|
|
362
431
|
let json2;
|
|
363
432
|
try {
|
|
364
|
-
json2 = await res.json();
|
|
365
|
-
} catch {
|
|
433
|
+
json2 = options.boundedRead ? await boundedJson(res, options.boundedRead.maxResponseBytes) : await res.json();
|
|
434
|
+
} catch (error) {
|
|
366
435
|
if (options.signal?.aborted) {
|
|
367
436
|
throw options.signal.reason;
|
|
368
437
|
}
|
|
438
|
+
if (options.boundedRead && controller.signal.aborted) {
|
|
439
|
+
throw new FabricError(
|
|
440
|
+
`Request timed out after ${this.timeoutMs}ms`,
|
|
441
|
+
0,
|
|
442
|
+
"TIMEOUT"
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
if (options.boundedRead && error instanceof FabricError) {
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
369
448
|
throw new FabricError(
|
|
370
449
|
`Unexpected response from server (status ${res.status})`,
|
|
371
450
|
res.status
|
|
372
451
|
);
|
|
452
|
+
} finally {
|
|
453
|
+
clearTimeout(timeoutHandle);
|
|
373
454
|
}
|
|
374
455
|
if (!res.ok) {
|
|
375
456
|
const body = json2;
|
|
@@ -433,28 +514,28 @@ var FabricHttpClient = class _FabricHttpClient {
|
|
|
433
514
|
} catch {
|
|
434
515
|
}
|
|
435
516
|
}
|
|
436
|
-
get(
|
|
517
|
+
get(path42, options = {}) {
|
|
437
518
|
return this.request(
|
|
438
519
|
"GET",
|
|
439
|
-
|
|
520
|
+
path42,
|
|
440
521
|
options.signal ? { signal: options.signal } : {}
|
|
441
522
|
);
|
|
442
523
|
}
|
|
443
|
-
post(
|
|
444
|
-
return this.request("POST",
|
|
524
|
+
post(path42, body, options = {}) {
|
|
525
|
+
return this.request("POST", path42, {
|
|
445
526
|
body,
|
|
446
527
|
idempotencyKey: options.idempotencyKey,
|
|
447
528
|
retry: options.retry
|
|
448
529
|
});
|
|
449
530
|
}
|
|
450
|
-
patch(
|
|
451
|
-
return this.request("PATCH",
|
|
531
|
+
patch(path42, body, options = {}) {
|
|
532
|
+
return this.request("PATCH", path42, {
|
|
452
533
|
body,
|
|
453
534
|
idempotencyKey: options.idempotencyKey
|
|
454
535
|
});
|
|
455
536
|
}
|
|
456
|
-
put(
|
|
457
|
-
return this.request("PUT",
|
|
537
|
+
put(path42, body, options = {}) {
|
|
538
|
+
return this.request("PUT", path42, {
|
|
458
539
|
body,
|
|
459
540
|
idempotencyKey: options.idempotencyKey,
|
|
460
541
|
retry: options.retry
|
|
@@ -465,8 +546,8 @@ var FabricHttpClient = class _FabricHttpClient {
|
|
|
465
546
|
* delete names the path and the version in its body). Without `body`,
|
|
466
547
|
* nothing is sent, as before.
|
|
467
548
|
*/
|
|
468
|
-
delete(
|
|
469
|
-
return this.request("DELETE",
|
|
549
|
+
delete(path42, body, options = {}) {
|
|
550
|
+
return this.request("DELETE", path42, {
|
|
470
551
|
body,
|
|
471
552
|
idempotencyKey: options.idempotencyKey
|
|
472
553
|
});
|
|
@@ -830,6 +911,16 @@ function buildQuery2(opts) {
|
|
|
830
911
|
return s ? `?${s}` : "";
|
|
831
912
|
}
|
|
832
913
|
|
|
914
|
+
// ../sdk/src/resources/instruction-repository-files.ts
|
|
915
|
+
function readPinnedRepositoryFiles(http, url, input, signal) {
|
|
916
|
+
const deadline = AbortSignal.timeout(12e4);
|
|
917
|
+
return http.request("POST", url, {
|
|
918
|
+
body: input,
|
|
919
|
+
signal: signal ? AbortSignal.any([signal, deadline]) : deadline,
|
|
920
|
+
boundedRead: { maxResponseBytes: 4 * 1024 * 1024 }
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
833
924
|
// ../sdk/src/resources/instructions.ts
|
|
834
925
|
var InstructionsResource = class {
|
|
835
926
|
constructor(http) {
|
|
@@ -862,9 +953,19 @@ var InstructionsResource = class {
|
|
|
862
953
|
* commit pin; repeat calls must carry that pin so a moving branch cannot
|
|
863
954
|
* mix tree metadata with later file bytes.
|
|
864
955
|
*/
|
|
865
|
-
listRepositoryFiles(projectId, options = {}) {
|
|
956
|
+
listRepositoryFiles(projectId, options = {}, request2 = {}) {
|
|
866
957
|
return this.http.get(
|
|
867
|
-
`/projects/${encodeURIComponent(projectId)}/instructions/repository/files${buildQuery3(options)}
|
|
958
|
+
`/projects/${encodeURIComponent(projectId)}/instructions/repository/files${buildQuery3(options)}`,
|
|
959
|
+
request2
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
/** Read-only POST, with at most two retries and a two-minute total budget. */
|
|
963
|
+
readRepositoryFiles(projectId, input, options = {}) {
|
|
964
|
+
return readPinnedRepositoryFiles(
|
|
965
|
+
this.http,
|
|
966
|
+
`/projects/${encodeURIComponent(projectId)}/instructions/repository/files/read${buildQuery3({ org: options.org })}`,
|
|
967
|
+
input,
|
|
968
|
+
options.signal
|
|
868
969
|
);
|
|
869
970
|
}
|
|
870
971
|
/** Read one text file from an already pinned direct repository commit. */
|
|
@@ -1091,14 +1192,14 @@ function buildQuery3(opts) {
|
|
|
1091
1192
|
|
|
1092
1193
|
// ../sdk/src/resources/integrations.ts
|
|
1093
1194
|
var RESOURCE_BUILTINS = /* @__PURE__ */ new Set(["list", "constructor", "then"]);
|
|
1094
|
-
function makeDotPath(http, slug,
|
|
1095
|
-
const fn = (args) => http.post(`/integrations/${slug}/${
|
|
1195
|
+
function makeDotPath(http, slug, path42) {
|
|
1196
|
+
const fn = (args) => http.post(`/integrations/${slug}/${path42.join(".")}`, args ?? {});
|
|
1096
1197
|
return new Proxy(fn, {
|
|
1097
1198
|
get(target, prop) {
|
|
1098
1199
|
if (prop in target || typeof prop !== "string") {
|
|
1099
1200
|
return Reflect.get(target, prop);
|
|
1100
1201
|
}
|
|
1101
|
-
return makeDotPath(http, slug, [...
|
|
1202
|
+
return makeDotPath(http, slug, [...path42, prop]);
|
|
1102
1203
|
}
|
|
1103
1204
|
});
|
|
1104
1205
|
}
|
|
@@ -1784,6 +1885,102 @@ function buildQuery8(opts) {
|
|
|
1784
1885
|
return s ? `?${s}` : "";
|
|
1785
1886
|
}
|
|
1786
1887
|
|
|
1888
|
+
// ../sdk/src/plugin-mapping.ts
|
|
1889
|
+
var ALWAYS_ON_FILES = ["CLAUDE.md", "AGENTS.md"];
|
|
1890
|
+
function segmentsBelowRoot(instructionPath) {
|
|
1891
|
+
const segments = instructionPath.split("/");
|
|
1892
|
+
return segments[0]?.toLowerCase() === ".claude" ? segments.slice(1) : segments;
|
|
1893
|
+
}
|
|
1894
|
+
function isMarkdown(name) {
|
|
1895
|
+
return name?.toLowerCase().endsWith(".md") ?? false;
|
|
1896
|
+
}
|
|
1897
|
+
function pluginPathFor(instructionPath) {
|
|
1898
|
+
const rest = segmentsBelowRoot(instructionPath);
|
|
1899
|
+
const folder = rest[0]?.toLowerCase();
|
|
1900
|
+
if (rest.length < 2) {
|
|
1901
|
+
return null;
|
|
1902
|
+
}
|
|
1903
|
+
const below = rest.slice(1);
|
|
1904
|
+
switch (folder) {
|
|
1905
|
+
case "skills":
|
|
1906
|
+
return below.length >= 2 ? `skills/${below.join("/")}` : null;
|
|
1907
|
+
case "agents":
|
|
1908
|
+
return below.length === 1 && isMarkdown(below[0]) ? `agents/${below[0]}` : null;
|
|
1909
|
+
case "commands":
|
|
1910
|
+
return `commands/${below.join("/")}`;
|
|
1911
|
+
default:
|
|
1912
|
+
return null;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
function projectRulePathFor(instructionPath) {
|
|
1916
|
+
const rest = segmentsBelowRoot(instructionPath);
|
|
1917
|
+
if (rest.length < 2 || rest[0]?.toLowerCase() !== "rules" || !isMarkdown(rest[rest.length - 1])) {
|
|
1918
|
+
return null;
|
|
1919
|
+
}
|
|
1920
|
+
return rest.slice(1).join("/");
|
|
1921
|
+
}
|
|
1922
|
+
function reasonFor(instructionPath) {
|
|
1923
|
+
const rest = segmentsBelowRoot(instructionPath);
|
|
1924
|
+
if (rest.length >= 2) {
|
|
1925
|
+
switch (rest[0]?.toLowerCase()) {
|
|
1926
|
+
case "skills":
|
|
1927
|
+
return "skill-needs-folder";
|
|
1928
|
+
case "agents":
|
|
1929
|
+
return "agent-must-be-markdown-file";
|
|
1930
|
+
case "rules":
|
|
1931
|
+
return "rule-must-be-markdown";
|
|
1932
|
+
default:
|
|
1933
|
+
break;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
return "outside-plugin-folders";
|
|
1937
|
+
}
|
|
1938
|
+
var byPath = (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
|
|
1939
|
+
function planPluginFiles(entries, tool) {
|
|
1940
|
+
const order = tool === "codex" ? ["AGENTS.md", "CLAUDE.md"] : ALWAYS_ON_FILES;
|
|
1941
|
+
const present = new Map(entries.map((entry) => [entry.path, entry]));
|
|
1942
|
+
const alwaysOn = order.find((name) => {
|
|
1943
|
+
const entry = present.get(name);
|
|
1944
|
+
return entry !== void 0 && entry.size !== 0;
|
|
1945
|
+
}) ?? null;
|
|
1946
|
+
const plan = {
|
|
1947
|
+
carried: [],
|
|
1948
|
+
rules: [],
|
|
1949
|
+
alwaysOn: alwaysOn === null ? null : { path: alwaysOn },
|
|
1950
|
+
leftOut: []
|
|
1951
|
+
};
|
|
1952
|
+
for (const entry of entries) {
|
|
1953
|
+
if (entry.path === alwaysOn) {
|
|
1954
|
+
continue;
|
|
1955
|
+
}
|
|
1956
|
+
if (ALWAYS_ON_FILES.includes(entry.path)) {
|
|
1957
|
+
plan.leftOut.push({
|
|
1958
|
+
path: entry.path,
|
|
1959
|
+
reason: "always-on-shadowed"
|
|
1960
|
+
});
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
const place = pluginPathFor(entry.path);
|
|
1964
|
+
const rule = projectRulePathFor(entry.path);
|
|
1965
|
+
if (place === null && rule === null) {
|
|
1966
|
+
plan.leftOut.push({
|
|
1967
|
+
path: entry.path,
|
|
1968
|
+
reason: reasonFor(entry.path)
|
|
1969
|
+
});
|
|
1970
|
+
} else if (entry.isText === false) {
|
|
1971
|
+
plan.leftOut.push({ path: entry.path, reason: "not-text" });
|
|
1972
|
+
} else if (rule !== null) {
|
|
1973
|
+
plan.rules.push({ path: entry.path, as: rule });
|
|
1974
|
+
} else if (place !== null) {
|
|
1975
|
+
plan.carried.push({ path: entry.path, as: place });
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
plan.carried.sort(byPath);
|
|
1979
|
+
plan.rules.sort(byPath);
|
|
1980
|
+
plan.leftOut.sort(byPath);
|
|
1981
|
+
return plan;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1787
1984
|
// ../sdk/src/index.ts
|
|
1788
1985
|
var FabricClient = class _FabricClient {
|
|
1789
1986
|
constructor(optionsOrHttp = {}) {
|
|
@@ -1848,9 +2045,11 @@ import Conf from "conf";
|
|
|
1848
2045
|
import { randomUUID } from "crypto";
|
|
1849
2046
|
import {
|
|
1850
2047
|
closeSync,
|
|
2048
|
+
constants,
|
|
2049
|
+
fstatSync,
|
|
2050
|
+
lstatSync,
|
|
1851
2051
|
openSync,
|
|
1852
|
-
|
|
1853
|
-
statSync,
|
|
2052
|
+
readSync,
|
|
1854
2053
|
unlinkSync,
|
|
1855
2054
|
writeSync
|
|
1856
2055
|
} from "fs";
|
|
@@ -1864,6 +2063,13 @@ var ExclusiveLockBusyError = class extends Error {
|
|
|
1864
2063
|
}
|
|
1865
2064
|
};
|
|
1866
2065
|
var DEFAULT_POLL_MS = 100;
|
|
2066
|
+
var MAX_LOCK_BYTES = 256;
|
|
2067
|
+
var LockIdentityChangedError = class extends Error {
|
|
2068
|
+
constructor() {
|
|
2069
|
+
super("The lock changed while it was being read.");
|
|
2070
|
+
this.name = "LockIdentityChangedError";
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
1867
2073
|
function sleep2(ms) {
|
|
1868
2074
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1869
2075
|
}
|
|
@@ -1885,14 +2091,12 @@ function takeOnce(lockPath2, staleMs) {
|
|
|
1885
2091
|
}
|
|
1886
2092
|
}
|
|
1887
2093
|
try {
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
}
|
|
1892
|
-
return "abandoned";
|
|
2094
|
+
const read = readLock(lockPath2);
|
|
2095
|
+
if (Date.now() - read.stats.mtimeMs > staleMs) {
|
|
2096
|
+
return ownerIsAlive(read.owner) ? "busy" : "abandoned";
|
|
1893
2097
|
}
|
|
1894
2098
|
} catch (error) {
|
|
1895
|
-
if (isNotFoundError(error)) {
|
|
2099
|
+
if (isNotFoundError(error) || error instanceof LockIdentityChangedError) {
|
|
1896
2100
|
return "retry";
|
|
1897
2101
|
}
|
|
1898
2102
|
throw error;
|
|
@@ -1916,13 +2120,65 @@ function ownerIsAlive(owner) {
|
|
|
1916
2120
|
return code === "EPERM" || code !== "ESRCH" && code !== "ENOENT";
|
|
1917
2121
|
}
|
|
1918
2122
|
}
|
|
2123
|
+
function readLock(lockPath2) {
|
|
2124
|
+
const descriptor = openSync(
|
|
2125
|
+
lockPath2,
|
|
2126
|
+
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
|
|
2127
|
+
);
|
|
2128
|
+
try {
|
|
2129
|
+
const stats = fstatSync(descriptor);
|
|
2130
|
+
if (!stats.isFile()) {
|
|
2131
|
+
throw new Error("The lock is not a regular file.");
|
|
2132
|
+
}
|
|
2133
|
+
if (stats.size > MAX_LOCK_BYTES) {
|
|
2134
|
+
throw new Error("The lock is too large.");
|
|
2135
|
+
}
|
|
2136
|
+
const buffer = Buffer.allocUnsafe(MAX_LOCK_BYTES + 1);
|
|
2137
|
+
let total = 0;
|
|
2138
|
+
while (total <= MAX_LOCK_BYTES) {
|
|
2139
|
+
const count = readSync(
|
|
2140
|
+
descriptor,
|
|
2141
|
+
buffer,
|
|
2142
|
+
total,
|
|
2143
|
+
MAX_LOCK_BYTES + 1 - total,
|
|
2144
|
+
total
|
|
2145
|
+
);
|
|
2146
|
+
if (count === 0) {
|
|
2147
|
+
break;
|
|
2148
|
+
}
|
|
2149
|
+
total += count;
|
|
2150
|
+
}
|
|
2151
|
+
if (total > MAX_LOCK_BYTES) {
|
|
2152
|
+
throw new Error("The lock is too large.");
|
|
2153
|
+
}
|
|
2154
|
+
const current = lstatSync(lockPath2);
|
|
2155
|
+
if (!current.isFile()) {
|
|
2156
|
+
throw new Error("The lock is not a regular file.");
|
|
2157
|
+
}
|
|
2158
|
+
if (current.size > MAX_LOCK_BYTES) {
|
|
2159
|
+
throw new Error("The lock is too large.");
|
|
2160
|
+
}
|
|
2161
|
+
if (current.dev !== stats.dev || current.ino !== stats.ino) {
|
|
2162
|
+
throw new LockIdentityChangedError();
|
|
2163
|
+
}
|
|
2164
|
+
return { owner: buffer.toString("utf8", 0, total), stats };
|
|
2165
|
+
} finally {
|
|
2166
|
+
closeSync(descriptor);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
function stillHeld(held, lockPath2) {
|
|
2170
|
+
const expected = fstatSync(held.descriptor);
|
|
2171
|
+
const read = readLock(lockPath2);
|
|
2172
|
+
return read.owner === held.owner && read.stats.dev === expected.dev && read.stats.ino === expected.ino;
|
|
2173
|
+
}
|
|
1919
2174
|
function release(held, lockPath2) {
|
|
1920
|
-
closeSync(held.descriptor);
|
|
1921
2175
|
try {
|
|
1922
|
-
if (
|
|
2176
|
+
if (stillHeld(held, lockPath2)) {
|
|
1923
2177
|
unlinkSync(lockPath2);
|
|
1924
2178
|
}
|
|
1925
2179
|
} catch {
|
|
2180
|
+
} finally {
|
|
2181
|
+
closeSync(held.descriptor);
|
|
1926
2182
|
}
|
|
1927
2183
|
}
|
|
1928
2184
|
async function withExclusiveLock(work, options) {
|
|
@@ -1948,6 +2204,10 @@ async function withExclusiveLock(work, options) {
|
|
|
1948
2204
|
}
|
|
1949
2205
|
}
|
|
1950
2206
|
try {
|
|
2207
|
+
options.signal?.throwIfAborted();
|
|
2208
|
+
if (!stillHeld(held, lockPath2)) {
|
|
2209
|
+
throw new Error("The lock changed before work could start.");
|
|
2210
|
+
}
|
|
1951
2211
|
return await work();
|
|
1952
2212
|
} finally {
|
|
1953
2213
|
release(held, lockPath2);
|
|
@@ -1978,6 +2238,9 @@ function withExclusiveLockSync(work, options) {
|
|
|
1978
2238
|
}
|
|
1979
2239
|
}
|
|
1980
2240
|
try {
|
|
2241
|
+
if (!stillHeld(held, lockPath2)) {
|
|
2242
|
+
throw new Error("The lock changed before work could start.");
|
|
2243
|
+
}
|
|
1981
2244
|
return work();
|
|
1982
2245
|
} finally {
|
|
1983
2246
|
release(held, lockPath2);
|
|
@@ -2414,8 +2677,8 @@ function sameOriginEndpoint(value, field2, issuer) {
|
|
|
2414
2677
|
}
|
|
2415
2678
|
function authorizationServerMetadataUrl(issuer) {
|
|
2416
2679
|
const url = new URL(issuer);
|
|
2417
|
-
const
|
|
2418
|
-
return `${url.origin}/.well-known/oauth-authorization-server${
|
|
2680
|
+
const path42 = url.pathname.replace(/\/+$/, "");
|
|
2681
|
+
return `${url.origin}/.well-known/oauth-authorization-server${path42}`;
|
|
2419
2682
|
}
|
|
2420
2683
|
async function discoverAuthorizationServer(baseUrl, options = {}) {
|
|
2421
2684
|
const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
|
|
@@ -2482,11 +2745,26 @@ var CallbackError = class extends Error {
|
|
|
2482
2745
|
this.name = "CallbackError";
|
|
2483
2746
|
}
|
|
2484
2747
|
};
|
|
2748
|
+
var DEFERRED_PAGE_TIMEOUT_MS = 60 * 1e3;
|
|
2749
|
+
var escapeHtml = (text3) => text3.replace(
|
|
2750
|
+
/[&<>"']/g,
|
|
2751
|
+
(char) => ({
|
|
2752
|
+
"&": "&",
|
|
2753
|
+
"<": "<",
|
|
2754
|
+
">": ">",
|
|
2755
|
+
'"': """,
|
|
2756
|
+
"'": "'"
|
|
2757
|
+
})[char] ?? char
|
|
2758
|
+
);
|
|
2485
2759
|
var FINAL_PAGE_HEADERS = {
|
|
2486
2760
|
"Content-Type": "text/html; charset=utf-8",
|
|
2487
2761
|
Connection: "close"
|
|
2488
2762
|
};
|
|
2489
|
-
var PAGE = (
|
|
2763
|
+
var PAGE = (rawTitle, rawBody) => {
|
|
2764
|
+
const title = escapeHtml(rawTitle);
|
|
2765
|
+
const body = escapeHtml(rawBody);
|
|
2766
|
+
return `<!doctype html><meta charset="utf-8"><title>${title}</title><body style="font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem"><h1 style="font-weight:400">${title}</h1><p>${body}</p></body>`;
|
|
2767
|
+
};
|
|
2490
2768
|
async function startLoopbackListener(options) {
|
|
2491
2769
|
const callbackPath = options.callbackPath ?? CALLBACK_PATH;
|
|
2492
2770
|
let settle = () => {
|
|
@@ -2502,6 +2780,32 @@ async function startLoopbackListener(options) {
|
|
|
2502
2780
|
});
|
|
2503
2781
|
result.catch(() => {
|
|
2504
2782
|
});
|
|
2783
|
+
let closed = false;
|
|
2784
|
+
let held = null;
|
|
2785
|
+
let heldTimer;
|
|
2786
|
+
const release2 = () => {
|
|
2787
|
+
held = null;
|
|
2788
|
+
clearTimeout(heldTimer);
|
|
2789
|
+
if (closed) {
|
|
2790
|
+
server.closeAllConnections();
|
|
2791
|
+
}
|
|
2792
|
+
};
|
|
2793
|
+
const finish = (page) => {
|
|
2794
|
+
const response = held;
|
|
2795
|
+
if (!response) {
|
|
2796
|
+
return Promise.resolve();
|
|
2797
|
+
}
|
|
2798
|
+
held = null;
|
|
2799
|
+
clearTimeout(heldTimer);
|
|
2800
|
+
return new Promise((resolve) => {
|
|
2801
|
+
response.once("close", resolve);
|
|
2802
|
+
if (response.writableEnded || response.destroyed) {
|
|
2803
|
+
resolve();
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
response.writeHead(200, FINAL_PAGE_HEADERS).end(PAGE(page.title, page.body), () => resolve());
|
|
2807
|
+
}).finally(release2);
|
|
2808
|
+
};
|
|
2505
2809
|
const server = createServer((request2, response) => {
|
|
2506
2810
|
const url = new URL(request2.url ?? "/", "http://127.0.0.1");
|
|
2507
2811
|
if (request2.method !== "GET" || url.pathname !== callbackPath) {
|
|
@@ -2543,6 +2847,22 @@ async function startLoopbackListener(options) {
|
|
|
2543
2847
|
);
|
|
2544
2848
|
return;
|
|
2545
2849
|
}
|
|
2850
|
+
if (options.deferPage) {
|
|
2851
|
+
const { fallback, timeoutMs } = options.deferPage;
|
|
2852
|
+
held = response;
|
|
2853
|
+
response.once("close", () => {
|
|
2854
|
+
if (held === response) {
|
|
2855
|
+
release2();
|
|
2856
|
+
}
|
|
2857
|
+
});
|
|
2858
|
+
heldTimer = setTimeout(
|
|
2859
|
+
() => void finish(fallback),
|
|
2860
|
+
timeoutMs ?? DEFERRED_PAGE_TIMEOUT_MS
|
|
2861
|
+
);
|
|
2862
|
+
heldTimer.unref();
|
|
2863
|
+
settle({ code, params: url.searchParams });
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2546
2866
|
response.writeHead(200, FINAL_PAGE_HEADERS).end(
|
|
2547
2867
|
PAGE(
|
|
2548
2868
|
options.successPage?.title ?? "Signed in",
|
|
@@ -2573,16 +2893,22 @@ async function startLoopbackListener(options) {
|
|
|
2573
2893
|
const onAbort = () => settle({ error: new Error("Sign-in was cancelled.") });
|
|
2574
2894
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2575
2895
|
const close = () => {
|
|
2896
|
+
closed = true;
|
|
2576
2897
|
clearTimeout(timer);
|
|
2577
2898
|
options.signal?.removeEventListener("abort", onAbort);
|
|
2578
|
-
|
|
2899
|
+
if (held) {
|
|
2900
|
+
server.closeIdleConnections();
|
|
2901
|
+
} else {
|
|
2902
|
+
server.closeAllConnections();
|
|
2903
|
+
}
|
|
2579
2904
|
server.close();
|
|
2580
2905
|
};
|
|
2581
2906
|
result.then(close, close);
|
|
2582
2907
|
return {
|
|
2583
2908
|
redirectUri: `http://127.0.0.1:${port}${callbackPath}`,
|
|
2584
2909
|
result,
|
|
2585
|
-
close
|
|
2910
|
+
close,
|
|
2911
|
+
finish
|
|
2586
2912
|
};
|
|
2587
2913
|
}
|
|
2588
2914
|
|
|
@@ -2695,7 +3021,8 @@ var UNUSABLE_CLIENT_ERRORS = /* @__PURE__ */ new Set([
|
|
|
2695
3021
|
"invalid_client",
|
|
2696
3022
|
"client_disabled",
|
|
2697
3023
|
"invalid_redirect",
|
|
2698
|
-
"unauthorized_client"
|
|
3024
|
+
"unauthorized_client",
|
|
3025
|
+
"invalid_scope"
|
|
2699
3026
|
]);
|
|
2700
3027
|
var OAuthLoginError = class extends Error {
|
|
2701
3028
|
constructor(message) {
|
|
@@ -4795,10 +5122,10 @@ var AGENT_NAME = "Fabric";
|
|
|
4795
5122
|
function registrationPath() {
|
|
4796
5123
|
return join(dirname(getConfigPath()), "chatgpt-plan-host.json");
|
|
4797
5124
|
}
|
|
4798
|
-
async function loadRegistrationStore(
|
|
5125
|
+
async function loadRegistrationStore(path42 = registrationPath()) {
|
|
4799
5126
|
let stored;
|
|
4800
5127
|
try {
|
|
4801
|
-
stored = JSON.parse(await readFile(
|
|
5128
|
+
stored = JSON.parse(await readFile(path42, "utf8"));
|
|
4802
5129
|
} catch {
|
|
4803
5130
|
stored = {};
|
|
4804
5131
|
}
|
|
@@ -4811,15 +5138,15 @@ async function loadRegistrationStore(path38 = registrationPath()) {
|
|
|
4811
5138
|
accounts: stored.subject && stored.clientId ? { [stored.subject]: { clientId: stored.clientId } } : {}
|
|
4812
5139
|
};
|
|
4813
5140
|
}
|
|
4814
|
-
async function saveAccountRegistration(account,
|
|
4815
|
-
const store = await loadRegistrationStore(
|
|
5141
|
+
async function saveAccountRegistration(account, path42 = registrationPath()) {
|
|
5142
|
+
const store = await loadRegistrationStore(path42);
|
|
4816
5143
|
const { hostId, subject, ...registration } = account;
|
|
4817
5144
|
const next = {
|
|
4818
5145
|
hostId,
|
|
4819
5146
|
accounts: { ...store.accounts, [subject]: registration }
|
|
4820
5147
|
};
|
|
4821
|
-
await mkdir(dirname(
|
|
4822
|
-
await writeFile(
|
|
5148
|
+
await mkdir(dirname(path42), { recursive: true, mode: 448 });
|
|
5149
|
+
await writeFile(path42, JSON.stringify(next), { mode: 384 });
|
|
4823
5150
|
}
|
|
4824
5151
|
function selectRegistration(store, account) {
|
|
4825
5152
|
if (account === void 0) {
|
|
@@ -4940,6 +5267,16 @@ function explainDenial(err) {
|
|
|
4940
5267
|
`${err.message}${description} ChatGPT refused it for that workspace: run \`fabric connect chatgpt\` again without --account (the same as --new-registration) and choose your personal Plus or Pro workspace when ChatGPT asks which one to sign in to.`
|
|
4941
5268
|
);
|
|
4942
5269
|
}
|
|
5270
|
+
var CHECK_TERMINAL_PAGE = {
|
|
5271
|
+
title: "Check your terminal",
|
|
5272
|
+
body: "Fabric is taking longer than expected to answer. Your terminal shows whether the ChatGPT plan was connected. You can close this tab."
|
|
5273
|
+
};
|
|
5274
|
+
function notConnectedPage(reason) {
|
|
5275
|
+
return {
|
|
5276
|
+
title: "Not connected",
|
|
5277
|
+
body: `${reason} Your terminal shows the same message. You can close this tab.`
|
|
5278
|
+
};
|
|
5279
|
+
}
|
|
4943
5280
|
async function attemptSignIn(registration, options) {
|
|
4944
5281
|
const verifier = createCodeVerifier();
|
|
4945
5282
|
const state = createState();
|
|
@@ -4947,7 +5284,10 @@ async function attemptSignIn(registration, options) {
|
|
|
4947
5284
|
const listener = await startLoopbackListener({
|
|
4948
5285
|
state,
|
|
4949
5286
|
callbackPath: CHATGPT_CALLBACK_PATH,
|
|
4950
|
-
preferredPort: options.preferredPort ?? CHATGPT_CALLBACK_PORT
|
|
5287
|
+
preferredPort: options.preferredPort ?? CHATGPT_CALLBACK_PORT,
|
|
5288
|
+
...options.holdBrowserPage && {
|
|
5289
|
+
deferPage: { fallback: CHECK_TERMINAL_PAGE }
|
|
5290
|
+
}
|
|
4951
5291
|
});
|
|
4952
5292
|
try {
|
|
4953
5293
|
options.open(
|
|
@@ -4961,27 +5301,38 @@ async function attemptSignIn(registration, options) {
|
|
|
4961
5301
|
})
|
|
4962
5302
|
);
|
|
4963
5303
|
const { code, params } = await listener.result;
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
5304
|
+
try {
|
|
5305
|
+
const clientId = issuedClientId(params, registration);
|
|
5306
|
+
const tokens = await exchangeCode(
|
|
5307
|
+
{
|
|
5308
|
+
clientId,
|
|
5309
|
+
code,
|
|
5310
|
+
codeVerifier: verifier,
|
|
5311
|
+
redirectUri: listener.redirectUri
|
|
5312
|
+
},
|
|
5313
|
+
options.fetchImpl
|
|
5314
|
+
);
|
|
5315
|
+
const { sub, email } = checkIdToken(tokens.id_token ?? "", {
|
|
5316
|
+
nonce,
|
|
5317
|
+
subject: registration.subject
|
|
5318
|
+
});
|
|
5319
|
+
await options.save?.({
|
|
5320
|
+
hostId: registration.hostId,
|
|
5321
|
+
subject: sub,
|
|
4967
5322
|
clientId,
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
})
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
clientId,
|
|
4982
|
-
...email !== void 0 && { email }
|
|
4983
|
-
});
|
|
4984
|
-
return { tokens, clientId, subject: sub, email };
|
|
5323
|
+
...email !== void 0 && { email }
|
|
5324
|
+
});
|
|
5325
|
+
return {
|
|
5326
|
+
tokens,
|
|
5327
|
+
clientId,
|
|
5328
|
+
subject: sub,
|
|
5329
|
+
email,
|
|
5330
|
+
finishBrowser: listener.finish
|
|
5331
|
+
};
|
|
5332
|
+
} catch (err) {
|
|
5333
|
+
await listener.finish(notConnectedPage(err.message));
|
|
5334
|
+
throw err;
|
|
5335
|
+
}
|
|
4985
5336
|
} finally {
|
|
4986
5337
|
listener.close();
|
|
4987
5338
|
}
|
|
@@ -5139,7 +5490,7 @@ async function approveInFabric(origin, sharedOrganizationSlug) {
|
|
|
5139
5490
|
function describeOrganizations(result) {
|
|
5140
5491
|
const enabled = result.organizations.filter((org) => org.enabled);
|
|
5141
5492
|
if (enabled.length === 0) {
|
|
5142
|
-
return "Choose where to use it in Fabric
|
|
5493
|
+
return "Choose where to use it in Fabric: Settings \u2192 AI Providers \u2192 Personal.";
|
|
5143
5494
|
}
|
|
5144
5495
|
return `Your own AI work in ${enabled.map((org) => org.name).join(", ")} now uses your ChatGPT plan.`;
|
|
5145
5496
|
}
|
|
@@ -5148,6 +5499,28 @@ function describeSharedAccount(result) {
|
|
|
5148
5499
|
const { organization, created } = result.shared;
|
|
5149
5500
|
return `${created ? "Connected" : "Reconnected"} the ChatGPT account${account} as a shared account of ${organization.name}. Choose which work it serves in ${organization.name}'s settings \u2192 AI providers.`;
|
|
5150
5501
|
}
|
|
5502
|
+
function describeConnected(result) {
|
|
5503
|
+
if ("shared" in result) {
|
|
5504
|
+
return describeSharedAccount(result);
|
|
5505
|
+
}
|
|
5506
|
+
return `Connected your ChatGPT plan${result.email ? ` (${result.email})` : ""} to Fabric.`;
|
|
5507
|
+
}
|
|
5508
|
+
function connectedPage(result) {
|
|
5509
|
+
const where = "shared" in result ? "" : ` ${describeOrganizations(result)}`;
|
|
5510
|
+
return {
|
|
5511
|
+
title: "Connected",
|
|
5512
|
+
body: `${describeConnected(result)}${where} You can close this tab.`
|
|
5513
|
+
};
|
|
5514
|
+
}
|
|
5515
|
+
function notConnectedMessage(reason) {
|
|
5516
|
+
return `
|
|
5517
|
+
\u2717 Not connected: ${reason}
|
|
5518
|
+
`;
|
|
5519
|
+
}
|
|
5520
|
+
function failConnect(reason) {
|
|
5521
|
+
process.stderr.write(notConnectedMessage(reason));
|
|
5522
|
+
process.exit(1);
|
|
5523
|
+
}
|
|
5151
5524
|
async function connect(opts) {
|
|
5152
5525
|
const optionsError = connectOptionsError(opts);
|
|
5153
5526
|
if (optionsError) {
|
|
@@ -5178,18 +5551,20 @@ async function connect(opts) {
|
|
|
5178
5551
|
try {
|
|
5179
5552
|
ticket = await approveInFabric(origin, sharedOrganizationSlug);
|
|
5180
5553
|
} catch (err) {
|
|
5181
|
-
|
|
5554
|
+
failConnect(err.message);
|
|
5182
5555
|
}
|
|
5183
5556
|
process.stdout.write("Approved in Fabric.\n");
|
|
5557
|
+
let signIn;
|
|
5184
5558
|
let result;
|
|
5185
5559
|
try {
|
|
5186
|
-
|
|
5560
|
+
signIn = await signInWithChatGpt({
|
|
5187
5561
|
registration,
|
|
5188
5562
|
open: (url) => open(url, "sign in with ChatGPT"),
|
|
5189
5563
|
...!sharedOrganizationSlug && { save: saveAccountRegistration },
|
|
5190
5564
|
onRetry: () => process.stdout.write(
|
|
5191
5565
|
"ChatGPT refused this machine's saved registration; signing in again with a new one.\n"
|
|
5192
|
-
)
|
|
5566
|
+
),
|
|
5567
|
+
holdBrowserPage: true
|
|
5193
5568
|
});
|
|
5194
5569
|
result = await uploadChatGptPlan({
|
|
5195
5570
|
origin,
|
|
@@ -5200,17 +5575,16 @@ async function connect(opts) {
|
|
|
5200
5575
|
})
|
|
5201
5576
|
});
|
|
5202
5577
|
} catch (err) {
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
);
|
|
5212
|
-
process.stdout.write(`${describeOrganizations(result)}
|
|
5578
|
+
const reason = err.message;
|
|
5579
|
+
await signIn?.finishBrowser(notConnectedPage(reason));
|
|
5580
|
+
failConnect(reason);
|
|
5581
|
+
}
|
|
5582
|
+
await signIn.finishBrowser(connectedPage(result));
|
|
5583
|
+
printSuccess(describeConnected(result));
|
|
5584
|
+
if (!("shared" in result)) {
|
|
5585
|
+
process.stdout.write(`${describeOrganizations(result)}
|
|
5213
5586
|
`);
|
|
5587
|
+
}
|
|
5214
5588
|
}
|
|
5215
5589
|
function buildConnectCommand() {
|
|
5216
5590
|
const connectCommand = new Command8("connect").description(
|
|
@@ -5314,7 +5688,7 @@ import path3 from "path";
|
|
|
5314
5688
|
|
|
5315
5689
|
// src/lib/instructions/safe-write.ts
|
|
5316
5690
|
import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
|
|
5317
|
-
import { constants } from "fs";
|
|
5691
|
+
import { constants as constants2 } from "fs";
|
|
5318
5692
|
import {
|
|
5319
5693
|
lstat,
|
|
5320
5694
|
mkdir as mkdir2,
|
|
@@ -5547,7 +5921,7 @@ async function openFileSafely(root, relativePath) {
|
|
|
5547
5921
|
try {
|
|
5548
5922
|
handle = await open2(
|
|
5549
5923
|
check.path,
|
|
5550
|
-
|
|
5924
|
+
constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK
|
|
5551
5925
|
);
|
|
5552
5926
|
} catch (error) {
|
|
5553
5927
|
if (error.code === "ENOENT") {
|
|
@@ -5593,40 +5967,57 @@ async function readFileSafely(root, relativePath, options = {}) {
|
|
|
5593
5967
|
mode: opened.stats.mode
|
|
5594
5968
|
};
|
|
5595
5969
|
}
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
if (opened.stats.size > options.maxBytes) {
|
|
5602
|
-
throw new Error(
|
|
5603
|
-
`${relativePath} is too large to read safely (maximum ${options.maxBytes} bytes).`
|
|
5604
|
-
);
|
|
5605
|
-
}
|
|
5606
|
-
const buffer = Buffer.allocUnsafe(options.maxBytes + 1);
|
|
5607
|
-
let total = 0;
|
|
5608
|
-
while (total <= options.maxBytes) {
|
|
5609
|
-
const { bytesRead } = await opened.handle.read(
|
|
5610
|
-
buffer,
|
|
5611
|
-
total,
|
|
5612
|
-
options.maxBytes + 1 - total,
|
|
5613
|
-
total
|
|
5614
|
-
);
|
|
5615
|
-
if (bytesRead === 0) {
|
|
5616
|
-
break;
|
|
5617
|
-
}
|
|
5618
|
-
total += bytesRead;
|
|
5619
|
-
}
|
|
5620
|
-
if (total > options.maxBytes) {
|
|
5621
|
-
throw new Error(
|
|
5622
|
-
`${relativePath} is too large to read safely (maximum ${options.maxBytes} bytes).`
|
|
5623
|
-
);
|
|
5624
|
-
}
|
|
5625
|
-
return { bytes: buffer.subarray(0, total), mode: opened.stats.mode };
|
|
5970
|
+
return await readOpenedFileBounded(
|
|
5971
|
+
opened.handle,
|
|
5972
|
+
options.maxBytes,
|
|
5973
|
+
relativePath
|
|
5974
|
+
);
|
|
5626
5975
|
} finally {
|
|
5627
5976
|
await opened.handle.close();
|
|
5628
5977
|
}
|
|
5629
5978
|
}
|
|
5979
|
+
async function readOpenedFileBounded(handle, maxBytes, label) {
|
|
5980
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
|
5981
|
+
throw new Error(
|
|
5982
|
+
"A safe file-read limit must be a non-negative integer."
|
|
5983
|
+
);
|
|
5984
|
+
}
|
|
5985
|
+
const stats = await handle.stat();
|
|
5986
|
+
if (!stats.isFile()) {
|
|
5987
|
+
throw new Error(`${label} is not a regular file.`);
|
|
5988
|
+
}
|
|
5989
|
+
const tooLarge = () => new Error(
|
|
5990
|
+
`${label} is too large to read safely (maximum ${maxBytes} bytes).`
|
|
5991
|
+
);
|
|
5992
|
+
if (stats.size > maxBytes) {
|
|
5993
|
+
throw tooLarge();
|
|
5994
|
+
}
|
|
5995
|
+
const buffer = Buffer.allocUnsafe(maxBytes + 1);
|
|
5996
|
+
let total = 0;
|
|
5997
|
+
while (total <= maxBytes) {
|
|
5998
|
+
const { bytesRead } = await handle.read(
|
|
5999
|
+
buffer,
|
|
6000
|
+
total,
|
|
6001
|
+
maxBytes + 1 - total,
|
|
6002
|
+
total
|
|
6003
|
+
);
|
|
6004
|
+
if (bytesRead === 0) {
|
|
6005
|
+
break;
|
|
6006
|
+
}
|
|
6007
|
+
total += bytesRead;
|
|
6008
|
+
}
|
|
6009
|
+
if (total > maxBytes) {
|
|
6010
|
+
throw tooLarge();
|
|
6011
|
+
}
|
|
6012
|
+
const after = await handle.stat();
|
|
6013
|
+
if (after.size > maxBytes) {
|
|
6014
|
+
throw tooLarge();
|
|
6015
|
+
}
|
|
6016
|
+
if (!after.isFile() || after.mode !== stats.mode) {
|
|
6017
|
+
throw new Error(`${label} changed while it was being read.`);
|
|
6018
|
+
}
|
|
6019
|
+
return { bytes: buffer.subarray(0, total), mode: stats.mode };
|
|
6020
|
+
}
|
|
5630
6021
|
var FINGERPRINT_BUFFER_BYTES = 64 * 1024;
|
|
5631
6022
|
async function fingerprintFileSafely(root, relativePath) {
|
|
5632
6023
|
const opened = await openFileSafely(root, relativePath);
|
|
@@ -5844,23 +6235,23 @@ function normalizeContextSourcePath(input) {
|
|
|
5844
6235
|
if (slashed.startsWith("/") || DRIVE_LETTER_PREFIX.test(slashed)) {
|
|
5845
6236
|
return { ok: false, reason: "absolute" };
|
|
5846
6237
|
}
|
|
5847
|
-
let
|
|
5848
|
-
while (
|
|
5849
|
-
|
|
6238
|
+
let path42 = slashed.replace(/\/{2,}/g, "/");
|
|
6239
|
+
while (path42.startsWith("./")) {
|
|
6240
|
+
path42 = path42.slice(2);
|
|
5850
6241
|
}
|
|
5851
|
-
if (
|
|
6242
|
+
if (path42.length === 0) {
|
|
5852
6243
|
return { ok: false, reason: "empty" };
|
|
5853
6244
|
}
|
|
5854
|
-
if (
|
|
6245
|
+
if (path42.endsWith("/")) {
|
|
5855
6246
|
return { ok: false, reason: "trailing-slash" };
|
|
5856
6247
|
}
|
|
5857
|
-
if (
|
|
6248
|
+
if (path42.split("/").some((segment) => segment === "." || segment === "..")) {
|
|
5858
6249
|
return { ok: false, reason: "dot-segment" };
|
|
5859
6250
|
}
|
|
5860
|
-
if (
|
|
6251
|
+
if (path42.length > MAX_CONTEXT_SOURCE_PATH_LENGTH) {
|
|
5861
6252
|
return { ok: false, reason: "too-long" };
|
|
5862
6253
|
}
|
|
5863
|
-
return { ok: true, sourcePath:
|
|
6254
|
+
return { ok: true, sourcePath: path42 };
|
|
5864
6255
|
}
|
|
5865
6256
|
|
|
5866
6257
|
// src/lib/context-sync/lock.ts
|
|
@@ -6058,7 +6449,7 @@ function buildContextIgnoreRules(input = {}) {
|
|
|
6058
6449
|
// src/lib/context-sync/walk.ts
|
|
6059
6450
|
import { lstat as lstat2, readdir } from "fs/promises";
|
|
6060
6451
|
import path5 from "path";
|
|
6061
|
-
function
|
|
6452
|
+
function byPath2(a, b) {
|
|
6062
6453
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
6063
6454
|
}
|
|
6064
6455
|
async function walkContextDirectory(root, rules) {
|
|
@@ -6111,16 +6502,16 @@ async function walkContextDirectory(root, rules) {
|
|
|
6111
6502
|
}
|
|
6112
6503
|
}
|
|
6113
6504
|
}
|
|
6114
|
-
walk.files.sort((a, b) =>
|
|
6115
|
-
walk.symlinks.sort(
|
|
6116
|
-
walk.special.sort(
|
|
6117
|
-
walk.unmatchable.sort(
|
|
6118
|
-
walk.ignored.sort((a, b) =>
|
|
6505
|
+
walk.files.sort((a, b) => byPath2(a.path, b.path));
|
|
6506
|
+
walk.symlinks.sort(byPath2);
|
|
6507
|
+
walk.special.sort(byPath2);
|
|
6508
|
+
walk.unmatchable.sort(byPath2);
|
|
6509
|
+
walk.ignored.sort((a, b) => byPath2(a.path, b.path));
|
|
6119
6510
|
return walk;
|
|
6120
6511
|
}
|
|
6121
6512
|
|
|
6122
6513
|
// src/lib/context-sync/plan.ts
|
|
6123
|
-
function
|
|
6514
|
+
function byPath3(a, b) {
|
|
6124
6515
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
6125
6516
|
}
|
|
6126
6517
|
async function readContextIgnore(root) {
|
|
@@ -6255,15 +6646,15 @@ async function computeContextPlan(input) {
|
|
|
6255
6646
|
skipped3.push({ path: entry.diskPath, reason: verdict.reason });
|
|
6256
6647
|
continue;
|
|
6257
6648
|
}
|
|
6258
|
-
const
|
|
6649
|
+
const sha2562 = hashContextContent(verdict.content);
|
|
6259
6650
|
const locked = lock?.files[entry.sourcePath];
|
|
6260
6651
|
const plannedFile = {
|
|
6261
6652
|
sourcePath: entry.sourcePath,
|
|
6262
6653
|
diskPath: entry.diskPath,
|
|
6263
|
-
sha256,
|
|
6654
|
+
sha256: sha2562,
|
|
6264
6655
|
bytes: read.bytes.byteLength
|
|
6265
6656
|
};
|
|
6266
|
-
if (locked?.sha256 ===
|
|
6657
|
+
if (locked?.sha256 === sha2562) {
|
|
6267
6658
|
unchangedLocal.push(entry.sourcePath);
|
|
6268
6659
|
} else {
|
|
6269
6660
|
push2.push(
|
|
@@ -6295,14 +6686,14 @@ async function computeContextPlan(input) {
|
|
|
6295
6686
|
removed.push(sourcePath);
|
|
6296
6687
|
}
|
|
6297
6688
|
}
|
|
6298
|
-
push2.sort((a, b) =>
|
|
6299
|
-
removed.sort(
|
|
6689
|
+
push2.sort((a, b) => byPath3(a.sourcePath, b.sourcePath));
|
|
6690
|
+
removed.sort(byPath3);
|
|
6300
6691
|
const moves = pairMoves(lock, removed, push2);
|
|
6301
6692
|
const moved = new Set(moves.map((move) => move.from));
|
|
6302
6693
|
const arrived = new Set(moves.map((move) => move.to));
|
|
6303
|
-
unchangedLocal.sort(
|
|
6304
|
-
forgotten.sort(
|
|
6305
|
-
skipped3.sort((a, b) =>
|
|
6694
|
+
unchangedLocal.sort(byPath3);
|
|
6695
|
+
forgotten.sort(byPath3);
|
|
6696
|
+
skipped3.sort((a, b) => byPath3(a.path, b.path));
|
|
6306
6697
|
return {
|
|
6307
6698
|
push: push2.filter((entry) => !arrived.has(entry.sourcePath)),
|
|
6308
6699
|
moves,
|
|
@@ -6664,7 +7055,7 @@ async function pushContextPlan(input) {
|
|
|
6664
7055
|
}
|
|
6665
7056
|
}
|
|
6666
7057
|
};
|
|
6667
|
-
const deleteEntry = async (sourcePath,
|
|
7058
|
+
const deleteEntry = async (sourcePath, sha2562) => {
|
|
6668
7059
|
const remove = (expectedContentHash) => input.client.contexts.deleteSyncedFile(
|
|
6669
7060
|
input.projectId,
|
|
6670
7061
|
{ sourcePath, expectedContentHash },
|
|
@@ -6687,7 +7078,7 @@ async function pushContextPlan(input) {
|
|
|
6687
7078
|
}
|
|
6688
7079
|
};
|
|
6689
7080
|
try {
|
|
6690
|
-
return fromAnswer(await remove(
|
|
7081
|
+
return fromAnswer(await remove(sha2562));
|
|
6691
7082
|
} catch (error) {
|
|
6692
7083
|
const managed = repositoryManagedOf(error);
|
|
6693
7084
|
if (managed !== null) {
|
|
@@ -6748,9 +7139,9 @@ async function pushContextPlan(input) {
|
|
|
6748
7139
|
}
|
|
6749
7140
|
};
|
|
6750
7141
|
const pruneTargetsSoFar = () => {
|
|
6751
|
-
const keptByMoves = results.map(moveKeptOldPath).filter((
|
|
6752
|
-
return [.../* @__PURE__ */ new Set([...input.plan.removed, ...keptByMoves])].filter((
|
|
6753
|
-
const locked = input.lock?.files[
|
|
7142
|
+
const keptByMoves = results.map(moveKeptOldPath).filter((path42) => path42 !== null);
|
|
7143
|
+
return [.../* @__PURE__ */ new Set([...input.plan.removed, ...keptByMoves])].filter((path42) => {
|
|
7144
|
+
const locked = input.lock?.files[path42];
|
|
6754
7145
|
return locked !== void 0 && locked.state !== "duplicate";
|
|
6755
7146
|
}).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
6756
7147
|
};
|
|
@@ -6821,12 +7212,12 @@ function nextContextLock(input) {
|
|
|
6821
7212
|
changed = true;
|
|
6822
7213
|
}
|
|
6823
7214
|
};
|
|
6824
|
-
const confirm2 = (sourcePath,
|
|
7215
|
+
const confirm2 = (sourcePath, sha2562, contextId) => {
|
|
6825
7216
|
const before = files[sourcePath];
|
|
6826
|
-
if (before?.state === void 0 && before?.sha256 ===
|
|
7217
|
+
if (before?.state === void 0 && before?.sha256 === sha2562 && before.contextId === contextId) {
|
|
6827
7218
|
return;
|
|
6828
7219
|
}
|
|
6829
|
-
files[sourcePath] = { sha256, contextId };
|
|
7220
|
+
files[sourcePath] = { sha256: sha2562, contextId };
|
|
6830
7221
|
changed = true;
|
|
6831
7222
|
};
|
|
6832
7223
|
for (const sourcePath of input.forgotten ?? []) {
|
|
@@ -7388,7 +7779,7 @@ function pruneSummary(results, targets) {
|
|
|
7388
7779
|
(r) => r.status === "deleted" || r.status === "already-gone"
|
|
7389
7780
|
).map((r) => r.sourcePath)
|
|
7390
7781
|
);
|
|
7391
|
-
const notDeleted = targets.filter((
|
|
7782
|
+
const notDeleted = targets.filter((path42) => !settled.has(path42)).length;
|
|
7392
7783
|
let text3 = `--prune: ${count("deleted")} deleted, ${count("already-gone")} already gone, ${notDeleted} not deleted`;
|
|
7393
7784
|
const first = results.find((r) => r.status === "delete-failed");
|
|
7394
7785
|
if (first && first.status === "delete-failed") {
|
|
@@ -7772,7 +8163,7 @@ function buildFramesCommand() {
|
|
|
7772
8163
|
|
|
7773
8164
|
// src/commands/instructions/index.ts
|
|
7774
8165
|
import { readdir as readdir4 } from "fs/promises";
|
|
7775
|
-
import
|
|
8166
|
+
import path39 from "path";
|
|
7776
8167
|
import { Command as Command13 } from "commander";
|
|
7777
8168
|
|
|
7778
8169
|
// src/lib/environment.ts
|
|
@@ -7785,15 +8176,15 @@ function runningInCi(env2 = process.env) {
|
|
|
7785
8176
|
}
|
|
7786
8177
|
|
|
7787
8178
|
// src/lib/instructions/adoption.ts
|
|
7788
|
-
import
|
|
8179
|
+
import path10 from "path";
|
|
7789
8180
|
|
|
7790
8181
|
// src/lib/instructions/checkout.ts
|
|
7791
|
-
import { realpath as
|
|
7792
|
-
import
|
|
8182
|
+
import { realpath as realpath3 } from "fs/promises";
|
|
8183
|
+
import path9 from "path";
|
|
7793
8184
|
|
|
7794
8185
|
// src/lib/instructions/git.ts
|
|
7795
|
-
import { lstat as
|
|
7796
|
-
import
|
|
8186
|
+
import { lstat as lstat4, stat } from "fs/promises";
|
|
8187
|
+
import path8 from "path";
|
|
7797
8188
|
|
|
7798
8189
|
// src/lib/instructions/git-literals.ts
|
|
7799
8190
|
var COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
@@ -8167,8 +8558,7 @@ async function changedTrackedPaths(root, deadline) {
|
|
|
8167
8558
|
}
|
|
8168
8559
|
|
|
8169
8560
|
// src/lib/instructions/git-write.ts
|
|
8170
|
-
import {
|
|
8171
|
-
import path8 from "path";
|
|
8561
|
+
import { realpath as realpath2 } from "fs/promises";
|
|
8172
8562
|
function fetchFailureOf(stderr) {
|
|
8173
8563
|
if (/config-env/i.test(stderr) && /unknown option|usage:/i.test(stderr)) {
|
|
8174
8564
|
return "old-git";
|
|
@@ -8380,24 +8770,22 @@ async function restorableBytes(root, file, entry, indexed, fileMode) {
|
|
|
8380
8770
|
if (indexed === null || indexed === void 0 || entry.mode === "120000") {
|
|
8381
8771
|
return null;
|
|
8382
8772
|
}
|
|
8383
|
-
const
|
|
8384
|
-
|
|
8385
|
-
|
|
8773
|
+
const read = await readFileSafely(root, file, {
|
|
8774
|
+
maxBytes: MAX_COMPARED_BYTES
|
|
8775
|
+
}).catch(() => null);
|
|
8776
|
+
if (read === null) {
|
|
8386
8777
|
return null;
|
|
8387
8778
|
}
|
|
8388
|
-
const executable = (
|
|
8779
|
+
const executable = (read.mode & 73) !== 0;
|
|
8389
8780
|
if (fileMode && executable !== (entry.mode === "100755")) {
|
|
8390
8781
|
return null;
|
|
8391
8782
|
}
|
|
8392
|
-
const local =
|
|
8393
|
-
if (local === null) {
|
|
8394
|
-
return null;
|
|
8395
|
-
}
|
|
8783
|
+
const local = Buffer.from(read.bytes);
|
|
8396
8784
|
const mine = local.toString("latin1");
|
|
8397
8785
|
if (mine.includes("\0") || indexed.includes("\0")) {
|
|
8398
8786
|
return null;
|
|
8399
8787
|
}
|
|
8400
|
-
return mine.replace(CRLF, "\n") === indexed.replace(CRLF, "\n") ? local : null;
|
|
8788
|
+
return mine.replace(CRLF, "\n") === indexed.replace(CRLF, "\n") ? { bytes: local, mode: read.mode } : null;
|
|
8401
8789
|
}
|
|
8402
8790
|
async function realBlockers(root, files, deadline) {
|
|
8403
8791
|
const scope = ["--literal-pathspecs", "-c", "core.quotepath=false"];
|
|
@@ -8520,9 +8908,11 @@ async function realBlockers(root, files, deadline) {
|
|
|
8520
8908
|
}
|
|
8521
8909
|
var RESTORE_AND_MERGE_FLOOR_MS = 1500;
|
|
8522
8910
|
async function stillUntouched(root, restorable) {
|
|
8523
|
-
for (const [file,
|
|
8524
|
-
const now2 = await
|
|
8525
|
-
|
|
8911
|
+
for (const [file, previous] of restorable) {
|
|
8912
|
+
const now2 = await readFileSafely(root, file, {
|
|
8913
|
+
maxBytes: MAX_COMPARED_BYTES
|
|
8914
|
+
}).catch(() => null);
|
|
8915
|
+
if (now2 === null || now2.mode !== previous.mode || !Buffer.from(now2.bytes).equals(previous.bytes)) {
|
|
8526
8916
|
return false;
|
|
8527
8917
|
}
|
|
8528
8918
|
}
|
|
@@ -8536,7 +8926,11 @@ async function fastForwardTo(root, sha, deadline) {
|
|
|
8536
8926
|
if (first.kind !== "failed" || first.reason !== "local-changes" || first.files.length === 0) {
|
|
8537
8927
|
return first;
|
|
8538
8928
|
}
|
|
8539
|
-
const
|
|
8929
|
+
const restoreRoot = await realpath2(root).catch(() => null);
|
|
8930
|
+
if (restoreRoot === null) {
|
|
8931
|
+
return first;
|
|
8932
|
+
}
|
|
8933
|
+
const judged = await realBlockers(restoreRoot, first.files, deadline);
|
|
8540
8934
|
if (judged === "ran-out-of-time") {
|
|
8541
8935
|
return { kind: "timed-out" };
|
|
8542
8936
|
}
|
|
@@ -8546,11 +8940,11 @@ async function fastForwardTo(root, sha, deadline) {
|
|
|
8546
8940
|
if (judged.real.length > 0) {
|
|
8547
8941
|
return { kind: "failed", reason: "local-changes", files: judged.real };
|
|
8548
8942
|
}
|
|
8549
|
-
if (deadline - Date.now() < RESTORE_AND_MERGE_FLOOR_MS || !await stillUntouched(
|
|
8943
|
+
if (deadline - Date.now() < RESTORE_AND_MERGE_FLOOR_MS || !await stillUntouched(restoreRoot, judged.restorable)) {
|
|
8550
8944
|
return first;
|
|
8551
8945
|
}
|
|
8552
8946
|
const restored = await runGit(
|
|
8553
|
-
|
|
8947
|
+
restoreRoot,
|
|
8554
8948
|
["--literal-pathspecs", "checkout", "--", ...first.files],
|
|
8555
8949
|
deadline,
|
|
8556
8950
|
{ write: true, unattended: true }
|
|
@@ -8558,7 +8952,7 @@ async function fastForwardTo(root, sha, deadline) {
|
|
|
8558
8952
|
if (restored.kind !== "exited" || restored.code !== 0) {
|
|
8559
8953
|
return first;
|
|
8560
8954
|
}
|
|
8561
|
-
return mergeOnce(
|
|
8955
|
+
return mergeOnce(restoreRoot, sha, deadline);
|
|
8562
8956
|
}
|
|
8563
8957
|
|
|
8564
8958
|
// src/lib/instructions/git.ts
|
|
@@ -8575,13 +8969,13 @@ var OPERATION_MARKERS = [
|
|
|
8575
8969
|
{ marker: "sequencer", operation: "cherry-pick" }
|
|
8576
8970
|
];
|
|
8577
8971
|
async function nearestExistingDirectory(dir) {
|
|
8578
|
-
let current =
|
|
8972
|
+
let current = path8.resolve(dir);
|
|
8579
8973
|
for (; ; ) {
|
|
8580
8974
|
const found = await stat(current).catch(() => null);
|
|
8581
8975
|
if (found?.isDirectory()) {
|
|
8582
8976
|
return current;
|
|
8583
8977
|
}
|
|
8584
|
-
const parent =
|
|
8978
|
+
const parent = path8.dirname(current);
|
|
8585
8979
|
if (parent === current) {
|
|
8586
8980
|
return null;
|
|
8587
8981
|
}
|
|
@@ -8591,10 +8985,10 @@ async function nearestExistingDirectory(dir) {
|
|
|
8591
8985
|
async function hasGitEntryAbove(dir) {
|
|
8592
8986
|
let current = dir;
|
|
8593
8987
|
for (; ; ) {
|
|
8594
|
-
if (await
|
|
8988
|
+
if (await lstat4(path8.join(current, ".git")).catch(() => null) !== null) {
|
|
8595
8989
|
return true;
|
|
8596
8990
|
}
|
|
8597
|
-
const parent =
|
|
8991
|
+
const parent = path8.dirname(current);
|
|
8598
8992
|
if (parent === current) {
|
|
8599
8993
|
return false;
|
|
8600
8994
|
}
|
|
@@ -8640,7 +9034,7 @@ async function findWorkTree(dir, deadline) {
|
|
|
8640
9034
|
kind: "ok",
|
|
8641
9035
|
value: {
|
|
8642
9036
|
toplevel,
|
|
8643
|
-
gitDir:
|
|
9037
|
+
gitDir: path8.resolve(cwd, gitDir),
|
|
8644
9038
|
shallow: shallow === "true",
|
|
8645
9039
|
superproject: superproject ? superproject : null
|
|
8646
9040
|
}
|
|
@@ -8766,7 +9160,7 @@ async function trackedPaths(root, relativePaths, deadline) {
|
|
|
8766
9160
|
return { kind: "ok", value: [] };
|
|
8767
9161
|
}
|
|
8768
9162
|
if (relativePaths.some(
|
|
8769
|
-
(relativePath) => relativePath === "" || relativePath.startsWith("-") ||
|
|
9163
|
+
(relativePath) => relativePath === "" || relativePath.startsWith("-") || path8.isAbsolute(relativePath) || path8.win32.isAbsolute(relativePath) || relativePath.split(/[\\/]/).some((part) => part === "..")
|
|
8770
9164
|
)) {
|
|
8771
9165
|
return {
|
|
8772
9166
|
kind: "unavailable",
|
|
@@ -8852,8 +9246,8 @@ async function operationInProgress(root, deadline) {
|
|
|
8852
9246
|
return { kind: "unavailable", reason: "git answered unexpectedly" };
|
|
8853
9247
|
}
|
|
8854
9248
|
for (const [index, { operation }] of OPERATION_MARKERS.entries()) {
|
|
8855
|
-
const marker =
|
|
8856
|
-
if (await
|
|
9249
|
+
const marker = path8.resolve(root, paths[index]);
|
|
9250
|
+
if (await lstat4(marker).catch(() => null) !== null) {
|
|
8857
9251
|
return { kind: "ok", value: operation };
|
|
8858
9252
|
}
|
|
8859
9253
|
}
|
|
@@ -8943,7 +9337,7 @@ async function lockFilesPresent(root, deadline) {
|
|
|
8943
9337
|
return { kind: "unavailable", reason: "git answered unexpectedly" };
|
|
8944
9338
|
}
|
|
8945
9339
|
for (const lock of paths) {
|
|
8946
|
-
if (await
|
|
9340
|
+
if (await lstat4(path8.resolve(root, lock)).catch(() => null) !== null) {
|
|
8947
9341
|
return { kind: "ok", value: true };
|
|
8948
9342
|
}
|
|
8949
9343
|
}
|
|
@@ -8991,7 +9385,7 @@ async function commonDir(root, deadline) {
|
|
|
8991
9385
|
if (result.value.code !== 0 || !dir) {
|
|
8992
9386
|
return { kind: "unavailable", reason: "git answered unexpectedly" };
|
|
8993
9387
|
}
|
|
8994
|
-
return { kind: "ok", value:
|
|
9388
|
+
return { kind: "ok", value: path8.resolve(root, dir) };
|
|
8995
9389
|
}
|
|
8996
9390
|
async function excludeFilePath(root, deadline) {
|
|
8997
9391
|
const result = await simple(
|
|
@@ -9006,7 +9400,7 @@ async function excludeFilePath(root, deadline) {
|
|
|
9006
9400
|
if (result.value.code !== 0 || !answer) {
|
|
9007
9401
|
return { kind: "unavailable", reason: "git answered unexpectedly" };
|
|
9008
9402
|
}
|
|
9009
|
-
return { kind: "ok", value:
|
|
9403
|
+
return { kind: "ok", value: path8.resolve(root, answer) };
|
|
9010
9404
|
}
|
|
9011
9405
|
async function isIgnored(root, relativePath, deadline) {
|
|
9012
9406
|
if (relativePath.startsWith("-") || relativePath === "") {
|
|
@@ -9636,15 +10030,15 @@ function downloadsIn(classification, manual) {
|
|
|
9636
10030
|
return manual && classification.class === "foreign";
|
|
9637
10031
|
}
|
|
9638
10032
|
function isSafeRootPath(rootPath) {
|
|
9639
|
-
if (
|
|
10033
|
+
if (path9.posix.isAbsolute(rootPath) || path9.win32.isAbsolute(rootPath) || rootPath.startsWith("~")) {
|
|
9640
10034
|
return false;
|
|
9641
10035
|
}
|
|
9642
10036
|
return !rootPath.split(/[\\/]/).some((segment) => segment === "..");
|
|
9643
10037
|
}
|
|
9644
10038
|
async function sameDirectory(a, b) {
|
|
9645
10039
|
const [left, right] = await Promise.all([
|
|
9646
|
-
|
|
9647
|
-
|
|
10040
|
+
realpath3(a).catch(() => null),
|
|
10041
|
+
realpath3(b).catch(() => null)
|
|
9648
10042
|
]);
|
|
9649
10043
|
return left !== null && left === right;
|
|
9650
10044
|
}
|
|
@@ -9730,7 +10124,7 @@ async function classify({
|
|
|
9730
10124
|
if (matching.length > 1) {
|
|
9731
10125
|
return { class: "ambiguous", remotes: matching };
|
|
9732
10126
|
}
|
|
9733
|
-
const mapped =
|
|
10127
|
+
const mapped = path9.join(root, repository.rootPath);
|
|
9734
10128
|
if (!await sameDirectory(destination, mapped)) {
|
|
9735
10129
|
return {
|
|
9736
10130
|
class: "unmapped",
|
|
@@ -10033,7 +10427,7 @@ function providerLoginCommand(provider, gitCommand) {
|
|
|
10033
10427
|
}
|
|
10034
10428
|
}
|
|
10035
10429
|
function folderForCommand(dest, relative) {
|
|
10036
|
-
const inside = relative.split(
|
|
10430
|
+
const inside = relative.split(path10.sep).join("/") || ".";
|
|
10037
10431
|
const typed = dest?.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
10038
10432
|
return shellQuote(
|
|
10039
10433
|
typed === void 0 || typed === "" ? inside : inside === "." ? typed : `${typed}/${inside}`
|
|
@@ -10075,9 +10469,9 @@ function adoptionFor(input) {
|
|
|
10075
10469
|
repo,
|
|
10076
10470
|
where: folderForCommand(
|
|
10077
10471
|
input.rerun.dest,
|
|
10078
|
-
|
|
10472
|
+
path10.relative(
|
|
10079
10473
|
input.destination,
|
|
10080
|
-
|
|
10474
|
+
path10.join(
|
|
10081
10475
|
classification.toplevel,
|
|
10082
10476
|
classification.rootPath
|
|
10083
10477
|
)
|
|
@@ -10109,17 +10503,27 @@ function adoptionFor(input) {
|
|
|
10109
10503
|
}
|
|
10110
10504
|
|
|
10111
10505
|
// src/lib/instructions/agent-mcp-config.ts
|
|
10112
|
-
import {
|
|
10113
|
-
import
|
|
10506
|
+
import { constants as constants3 } from "fs";
|
|
10507
|
+
import { open as open3 } from "fs/promises";
|
|
10508
|
+
import path11 from "path";
|
|
10114
10509
|
var LEGACY_SERVER_NAME = "fabric";
|
|
10115
10510
|
var MAX_CONFIG_BYTES = 16 * 1024 * 1024;
|
|
10116
10511
|
async function readText(file) {
|
|
10117
10512
|
try {
|
|
10118
|
-
const
|
|
10119
|
-
|
|
10120
|
-
|
|
10513
|
+
const handle = await open3(
|
|
10514
|
+
file,
|
|
10515
|
+
constants3.O_RDONLY | constants3.O_NONBLOCK
|
|
10516
|
+
);
|
|
10517
|
+
try {
|
|
10518
|
+
const read = await readOpenedFileBounded(
|
|
10519
|
+
handle,
|
|
10520
|
+
MAX_CONFIG_BYTES,
|
|
10521
|
+
file
|
|
10522
|
+
);
|
|
10523
|
+
return { kind: "text", text: read.bytes.toString("utf8") };
|
|
10524
|
+
} finally {
|
|
10525
|
+
await handle.close();
|
|
10121
10526
|
}
|
|
10122
|
-
return { kind: "text", text: await readFile4(file, "utf8") };
|
|
10123
10527
|
} catch (error) {
|
|
10124
10528
|
return error.code === "ENOENT" ? { kind: "absent" } : { kind: "unusable" };
|
|
10125
10529
|
}
|
|
@@ -10149,7 +10553,7 @@ async function readClaudeServers(input) {
|
|
|
10149
10553
|
return { state: "unlocated" };
|
|
10150
10554
|
}
|
|
10151
10555
|
const servers = [];
|
|
10152
|
-
const config2 = await readText(
|
|
10556
|
+
const config2 = await readText(path11.join(directory, ".claude.json"));
|
|
10153
10557
|
if (config2.kind === "unusable") {
|
|
10154
10558
|
return { state: "unreadable" };
|
|
10155
10559
|
}
|
|
@@ -10177,7 +10581,7 @@ async function readClaudeServers(input) {
|
|
|
10177
10581
|
servers.push({ scope: "user", ...found });
|
|
10178
10582
|
}
|
|
10179
10583
|
}
|
|
10180
|
-
const shared = await readText(
|
|
10584
|
+
const shared = await readText(path11.join(input.cwd, ".mcp.json"));
|
|
10181
10585
|
if (shared.kind === "text") {
|
|
10182
10586
|
try {
|
|
10183
10587
|
for (const found of serversIn(
|
|
@@ -10196,11 +10600,11 @@ var CODEX_SERVER_SUBTABLE = /^\[mcp_servers\.(?:"[^"]+"|'[^']+'|[A-Za-z0-9_-]+)\
|
|
|
10196
10600
|
var CODEX_OTHER_HEADER = /^\[{1,2}\s*mcp_servers\b/;
|
|
10197
10601
|
var CODEX_OTHER_KEY = /^mcp_servers\b/;
|
|
10198
10602
|
async function readCodexServers(input) {
|
|
10199
|
-
const directory = input.env.CODEX_HOME ?? (input.home === null ? void 0 :
|
|
10603
|
+
const directory = input.env.CODEX_HOME ?? (input.home === null ? void 0 : path11.join(input.home, ".codex"));
|
|
10200
10604
|
if (directory === void 0) {
|
|
10201
10605
|
return { state: "unlocated" };
|
|
10202
10606
|
}
|
|
10203
|
-
const config2 = await readText(
|
|
10607
|
+
const config2 = await readText(path11.join(directory, "config.toml"));
|
|
10204
10608
|
if (config2.kind === "absent") {
|
|
10205
10609
|
return { state: "read", servers: [] };
|
|
10206
10610
|
}
|
|
@@ -10351,7 +10755,7 @@ function registrationOf(servers, name) {
|
|
|
10351
10755
|
import { createHash as createHash4 } from "crypto";
|
|
10352
10756
|
import { mkdir as mkdir3 } from "fs/promises";
|
|
10353
10757
|
import { tmpdir } from "os";
|
|
10354
|
-
import
|
|
10758
|
+
import path12 from "path";
|
|
10355
10759
|
|
|
10356
10760
|
// src/lib/instructions/identifiers.ts
|
|
10357
10761
|
var IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
@@ -10360,11 +10764,11 @@ function isIdentifier(value) {
|
|
|
10360
10764
|
}
|
|
10361
10765
|
|
|
10362
10766
|
// src/lib/instructions/hook.ts
|
|
10363
|
-
var CLAUDE_SETTINGS_RELATIVE_PATH =
|
|
10767
|
+
var CLAUDE_SETTINGS_RELATIVE_PATH = path12.join(
|
|
10364
10768
|
".claude",
|
|
10365
10769
|
"settings.local.json"
|
|
10366
10770
|
);
|
|
10367
|
-
var CODEX_HOOKS_RELATIVE_PATH =
|
|
10771
|
+
var CODEX_HOOKS_RELATIVE_PATH = path12.join(".codex", "hooks.json");
|
|
10368
10772
|
var CLAUDE_SETTINGS_POSIX_PATH = ".claude/settings.local.json";
|
|
10369
10773
|
var CODEX_HOOKS_POSIX_PATH = ".codex/hooks.json";
|
|
10370
10774
|
var HOOK_TIMEOUT_SECONDS = 15;
|
|
@@ -10381,10 +10785,10 @@ function subcommandsForEvent(event) {
|
|
|
10381
10785
|
return event === "Stop" ? STOP_SUBCOMMANDS : SESSION_START_SUBCOMMANDS;
|
|
10382
10786
|
}
|
|
10383
10787
|
async function withHookConfigLock(root, posixPath, work) {
|
|
10384
|
-
const directory =
|
|
10788
|
+
const directory = path12.join(tmpdir(), "fabric-instructions-hook-locks");
|
|
10385
10789
|
await mkdir3(directory, { recursive: true });
|
|
10386
|
-
const identity = createHash4("sha256").update(
|
|
10387
|
-
const lockPath2 =
|
|
10790
|
+
const identity = createHash4("sha256").update(path12.resolve(root)).update("\0").update(posixPath).digest("hex");
|
|
10791
|
+
const lockPath2 = path12.join(directory, `${identity}.lock`);
|
|
10388
10792
|
try {
|
|
10389
10793
|
return await withExclusiveLock(work, {
|
|
10390
10794
|
lockPath: lockPath2,
|
|
@@ -10439,7 +10843,7 @@ async function assertKeyStaysOutside(destination, configPath) {
|
|
|
10439
10843
|
}
|
|
10440
10844
|
}
|
|
10441
10845
|
async function readHookConfig(root, hookTarget) {
|
|
10442
|
-
const settingsPath =
|
|
10846
|
+
const settingsPath = path12.join(root, hookTarget.relativePath);
|
|
10443
10847
|
let existingRaw;
|
|
10444
10848
|
try {
|
|
10445
10849
|
const existing = await readFileSafely(root, hookTarget.posixPath, {
|
|
@@ -10518,7 +10922,7 @@ function pruneMatching(groups, projectId, subcommands) {
|
|
|
10518
10922
|
async function mergeCommandHook(input) {
|
|
10519
10923
|
const hookTarget = hookTargetFor(input.tool ?? "claude-code");
|
|
10520
10924
|
return withHookConfigLock(input.root, hookTarget.posixPath, async () => {
|
|
10521
|
-
const settingsPath =
|
|
10925
|
+
const settingsPath = path12.join(input.root, hookTarget.relativePath);
|
|
10522
10926
|
const { settings, existingRaw } = await readHookConfig(
|
|
10523
10927
|
input.root,
|
|
10524
10928
|
hookTarget
|
|
@@ -10562,7 +10966,7 @@ async function mergeCommandHook(input) {
|
|
|
10562
10966
|
async function updateHookConfig(input) {
|
|
10563
10967
|
const hookTarget = hookTargetFor(input.tool);
|
|
10564
10968
|
return withHookConfigLock(input.root, hookTarget.posixPath, async () => {
|
|
10565
|
-
const settingsPath =
|
|
10969
|
+
const settingsPath = path12.join(input.root, hookTarget.relativePath);
|
|
10566
10970
|
const { settings, existingRaw } = await readHookConfig(
|
|
10567
10971
|
input.root,
|
|
10568
10972
|
hookTarget
|
|
@@ -10604,7 +11008,7 @@ async function mergeSessionStartHook(input) {
|
|
|
10604
11008
|
async function removeCommandHook(input) {
|
|
10605
11009
|
const hookTarget = hookTargetFor(input.tool ?? "claude-code");
|
|
10606
11010
|
return withHookConfigLock(input.root, hookTarget.posixPath, async () => {
|
|
10607
|
-
const settingsPath =
|
|
11011
|
+
const settingsPath = path12.join(input.root, hookTarget.relativePath);
|
|
10608
11012
|
const { settings, existingRaw } = await readHookConfig(
|
|
10609
11013
|
input.root,
|
|
10610
11014
|
hookTarget
|
|
@@ -11253,8 +11657,8 @@ function agentMcpSummaryLines(results) {
|
|
|
11253
11657
|
import { spawn as spawn3 } from "child_process";
|
|
11254
11658
|
|
|
11255
11659
|
// src/lib/instructions/path-lookup.ts
|
|
11256
|
-
import { stat as
|
|
11257
|
-
import
|
|
11660
|
+
import { stat as stat2 } from "fs/promises";
|
|
11661
|
+
import path13 from "path";
|
|
11258
11662
|
var MAX_PATH_ENTRIES = 256;
|
|
11259
11663
|
var MAX_PATHEXT_ENTRIES = 32;
|
|
11260
11664
|
var DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
|
|
@@ -11268,7 +11672,7 @@ function readVariable(lookup, name) {
|
|
|
11268
11672
|
return key === void 0 ? void 0 : lookup.env[key];
|
|
11269
11673
|
}
|
|
11270
11674
|
function pathApi(platform) {
|
|
11271
|
-
return platform === "win32" ?
|
|
11675
|
+
return platform === "win32" ? path13.win32 : path13.posix;
|
|
11272
11676
|
}
|
|
11273
11677
|
function searchDirectories(lookup) {
|
|
11274
11678
|
const api = pathApi(lookup.platform);
|
|
@@ -11290,7 +11694,7 @@ function windowsExtensions(lookup) {
|
|
|
11290
11694
|
const raw = readVariable(lookup, "PATHEXT") ?? DEFAULT_PATHEXT;
|
|
11291
11695
|
return raw.split(";").map((extension) => extension.trim()).filter((extension) => /^\.[A-Za-z0-9]+$/.test(extension)).slice(0, MAX_PATHEXT_ENTRIES);
|
|
11292
11696
|
}
|
|
11293
|
-
async function isExecutableFile(candidate, platform, statCandidate =
|
|
11697
|
+
async function isExecutableFile(candidate, platform, statCandidate = stat2) {
|
|
11294
11698
|
try {
|
|
11295
11699
|
const stats = await statCandidate(candidate);
|
|
11296
11700
|
if (!stats.isFile()) {
|
|
@@ -11464,8 +11868,8 @@ import { createHash as createHash5 } from "crypto";
|
|
|
11464
11868
|
import { chmod } from "fs/promises";
|
|
11465
11869
|
|
|
11466
11870
|
// src/lib/instructions/lock.ts
|
|
11467
|
-
import { readFile as
|
|
11468
|
-
import
|
|
11871
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
11872
|
+
import path14 from "path";
|
|
11469
11873
|
var LOCK_DIRECTORY = ".fabric";
|
|
11470
11874
|
var LOCK_FILENAME = "instructions.lock";
|
|
11471
11875
|
var LOCK_VERSION = 3;
|
|
@@ -11473,13 +11877,13 @@ var READABLE_LOCK_VERSIONS = [1, 2, LOCK_VERSION];
|
|
|
11473
11877
|
var LOCK_RELATIVE_PATH = `${LOCK_DIRECTORY}/${LOCK_FILENAME}`;
|
|
11474
11878
|
var SHA256_HEX2 = /^[0-9a-f]{64}$/;
|
|
11475
11879
|
function lockPath(destination) {
|
|
11476
|
-
return
|
|
11880
|
+
return path14.join(destination, LOCK_DIRECTORY, LOCK_FILENAME);
|
|
11477
11881
|
}
|
|
11478
|
-
async function
|
|
11882
|
+
async function readLock2(destination) {
|
|
11479
11883
|
const file = lockPath(destination);
|
|
11480
11884
|
let raw;
|
|
11481
11885
|
try {
|
|
11482
|
-
raw = await
|
|
11886
|
+
raw = await readFile3(file, "utf8");
|
|
11483
11887
|
} catch (error) {
|
|
11484
11888
|
if (error.code === "ENOENT") {
|
|
11485
11889
|
return null;
|
|
@@ -12081,11 +12485,11 @@ function extractBundle(archive, wanted) {
|
|
|
12081
12485
|
}
|
|
12082
12486
|
|
|
12083
12487
|
// src/lib/instructions/clone-target.ts
|
|
12084
|
-
import { lstat as
|
|
12085
|
-
import
|
|
12488
|
+
import { lstat as lstat5 } from "fs/promises";
|
|
12489
|
+
import path15 from "path";
|
|
12086
12490
|
async function resolveCloneFolder(base, folder) {
|
|
12087
|
-
const target =
|
|
12088
|
-
const stats = await
|
|
12491
|
+
const target = path15.resolve(base, folder);
|
|
12492
|
+
const stats = await lstat5(target).catch((error) => {
|
|
12089
12493
|
if (error.code === "ENOENT") {
|
|
12090
12494
|
return null;
|
|
12091
12495
|
}
|
|
@@ -12102,8 +12506,8 @@ async function resolveCloneFolder(base, folder) {
|
|
|
12102
12506
|
|
|
12103
12507
|
// src/lib/instructions/doctor.ts
|
|
12104
12508
|
import { createHash as createHash9 } from "crypto";
|
|
12105
|
-
import { stat as
|
|
12106
|
-
import
|
|
12509
|
+
import { stat as stat3 } from "fs/promises";
|
|
12510
|
+
import path17 from "path";
|
|
12107
12511
|
|
|
12108
12512
|
// src/lib/instructions/file-downloads.ts
|
|
12109
12513
|
import { createHash as createHash6 } from "crypto";
|
|
@@ -12448,13 +12852,13 @@ function isTooLarge(error) {
|
|
|
12448
12852
|
|
|
12449
12853
|
// src/lib/instructions/staged-files.ts
|
|
12450
12854
|
import { createHash as createHash8 } from "crypto";
|
|
12451
|
-
import { mkdtemp, realpath as
|
|
12855
|
+
import { mkdtemp, realpath as realpath4, rm } from "fs/promises";
|
|
12452
12856
|
import { tmpdir as tmpdir2 } from "os";
|
|
12453
|
-
import
|
|
12857
|
+
import path16 from "path";
|
|
12454
12858
|
var IN_MEMORY_SNAPSHOT_BYTES = 50 * 1024 * 1024;
|
|
12455
12859
|
async function stageFilesByUrl(options) {
|
|
12456
|
-
const root = await
|
|
12457
|
-
await mkdtemp(
|
|
12860
|
+
const root = await realpath4(
|
|
12861
|
+
await mkdtemp(path16.join(tmpdir2(), "fabric-instructions-"))
|
|
12458
12862
|
);
|
|
12459
12863
|
const files = /* @__PURE__ */ new Map();
|
|
12460
12864
|
const dispose = () => rm(root, { recursive: true, force: true });
|
|
@@ -12496,7 +12900,7 @@ async function stageFilesByUrl(options) {
|
|
|
12496
12900
|
// src/lib/instructions/doctor.ts
|
|
12497
12901
|
var MAX_ITEMS = 50;
|
|
12498
12902
|
var MAX_NAMES_IN_FIX = 20;
|
|
12499
|
-
var
|
|
12903
|
+
var MAX_LOCK_BYTES2 = 16 * 1024 * 1024;
|
|
12500
12904
|
var MAX_DOWNLOAD_BYTES = 52428800 + 1048576 + 5e3 * (512 + 2 * 1024);
|
|
12501
12905
|
var PROBE_TIMEOUT_MS = 5e3;
|
|
12502
12906
|
var PROBE_BUDGET_MS = 15e3;
|
|
@@ -12654,7 +13058,7 @@ function loginCommand2(input) {
|
|
|
12654
13058
|
) ?? fabricCommand("auth login");
|
|
12655
13059
|
}
|
|
12656
13060
|
async function isRegularFile(file) {
|
|
12657
|
-
return (await
|
|
13061
|
+
return (await stat3(file).catch(() => null))?.isFile() ?? false;
|
|
12658
13062
|
}
|
|
12659
13063
|
var Commands = class {
|
|
12660
13064
|
constructor(input) {
|
|
@@ -13154,7 +13558,7 @@ function checkPublished(access) {
|
|
|
13154
13558
|
}
|
|
13155
13559
|
async function readLockBounded(root) {
|
|
13156
13560
|
try {
|
|
13157
|
-
const lock = await readLockSafely(root, { maxBytes:
|
|
13561
|
+
const lock = await readLockSafely(root, { maxBytes: MAX_LOCK_BYTES2 });
|
|
13158
13562
|
return lock === null ? { kind: "absent" } : { kind: "ok", lock };
|
|
13159
13563
|
} catch (error) {
|
|
13160
13564
|
if (error instanceof LockReadRefusedError) {
|
|
@@ -13186,7 +13590,7 @@ function unreadableLockCheck(input, commands, reason) {
|
|
|
13186
13590
|
"lock",
|
|
13187
13591
|
"machine",
|
|
13188
13592
|
"fail",
|
|
13189
|
-
reason === "too-large" ? `the lock is larger than ${
|
|
13593
|
+
reason === "too-large" ? `the lock is larger than ${MAX_LOCK_BYTES2} bytes` : "the lock is unreadable",
|
|
13190
13594
|
{
|
|
13191
13595
|
fix: fixOf(
|
|
13192
13596
|
`the lock at ${where} is damaged: remove it, then run this sync to take a fresh copy`,
|
|
@@ -14383,13 +14787,13 @@ async function commandResolves(input, lookup, command) {
|
|
|
14383
14787
|
if (TOOL_NAME.test(command)) {
|
|
14384
14788
|
return await isOnPath(command, lookup) ? "found" : "missing";
|
|
14385
14789
|
}
|
|
14386
|
-
const api = input.platform === "win32" ?
|
|
14790
|
+
const api = input.platform === "win32" ? path17.win32 : path17.posix;
|
|
14387
14791
|
if (api.isAbsolute(command)) {
|
|
14388
14792
|
return await isExecutableFile(command, input.platform) ? "found" : "missing";
|
|
14389
14793
|
}
|
|
14390
14794
|
if (command.includes("/") || command.includes("\\")) {
|
|
14391
14795
|
return await isExecutableFile(
|
|
14392
|
-
|
|
14796
|
+
path17.resolve(input.root, command),
|
|
14393
14797
|
input.platform
|
|
14394
14798
|
) ? "found" : "missing";
|
|
14395
14799
|
}
|
|
@@ -14566,13 +14970,13 @@ function formatDoctorText(report, destination) {
|
|
|
14566
14970
|
// src/lib/instructions/drop-lock.ts
|
|
14567
14971
|
import { createHash as createHash10 } from "crypto";
|
|
14568
14972
|
import { rmdir } from "fs/promises";
|
|
14569
|
-
import
|
|
14973
|
+
import path18 from "path";
|
|
14570
14974
|
var LOCK_RELATIVE = `${LOCK_DIRECTORY}/${LOCK_FILENAME}`;
|
|
14571
|
-
var
|
|
14975
|
+
var MAX_LOCK_BYTES3 = 16 * 1024 * 1024;
|
|
14572
14976
|
async function dropOwnLock(root, projectId) {
|
|
14573
14977
|
try {
|
|
14574
14978
|
const read = await readFileSafely(root, LOCK_RELATIVE, {
|
|
14575
|
-
maxBytes:
|
|
14979
|
+
maxBytes: MAX_LOCK_BYTES3
|
|
14576
14980
|
});
|
|
14577
14981
|
if (read === null) {
|
|
14578
14982
|
return false;
|
|
@@ -14581,12 +14985,12 @@ async function dropOwnLock(root, projectId) {
|
|
|
14581
14985
|
if (lock.projectId !== projectId) {
|
|
14582
14986
|
return false;
|
|
14583
14987
|
}
|
|
14584
|
-
const
|
|
14585
|
-
const outcome = await deleteFileSafely(root, LOCK_RELATIVE,
|
|
14988
|
+
const sha2562 = createHash10("sha256").update(read.bytes).digest("hex");
|
|
14989
|
+
const outcome = await deleteFileSafely(root, LOCK_RELATIVE, sha2562);
|
|
14586
14990
|
if (outcome !== "deleted") {
|
|
14587
14991
|
return false;
|
|
14588
14992
|
}
|
|
14589
|
-
await rmdir(
|
|
14993
|
+
await rmdir(path18.join(root, LOCK_DIRECTORY)).catch(() => void 0);
|
|
14590
14994
|
return true;
|
|
14591
14995
|
} catch {
|
|
14592
14996
|
return false;
|
|
@@ -14678,8 +15082,8 @@ function replacePath(text3, target, replacement) {
|
|
|
14678
15082
|
}
|
|
14679
15083
|
|
|
14680
15084
|
// src/lib/instructions/fast-forward-run.ts
|
|
14681
|
-
import { mkdir as mkdir6, realpath as
|
|
14682
|
-
import
|
|
15085
|
+
import { mkdir as mkdir6, realpath as realpath5 } from "fs/promises";
|
|
15086
|
+
import path21 from "path";
|
|
14683
15087
|
|
|
14684
15088
|
// src/lib/instructions/fast-forward.ts
|
|
14685
15089
|
function fastForwardEligibility(facts) {
|
|
@@ -14868,10 +15272,10 @@ function fastForwardLines(outcome, context) {
|
|
|
14868
15272
|
}
|
|
14869
15273
|
|
|
14870
15274
|
// src/lib/instructions/fast-forward-memory.ts
|
|
14871
|
-
import { mkdir as mkdir4, readFile as
|
|
14872
|
-
import
|
|
15275
|
+
import { mkdir as mkdir4, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
15276
|
+
import path19 from "path";
|
|
14873
15277
|
function noticeFile(commonDir2) {
|
|
14874
|
-
return
|
|
15278
|
+
return path19.join(commonDir2, "fabric", "ff-notice.json");
|
|
14875
15279
|
}
|
|
14876
15280
|
function isRecord4(value) {
|
|
14877
15281
|
if (typeof value !== "object" || value === null) {
|
|
@@ -14882,7 +15286,7 @@ function isRecord4(value) {
|
|
|
14882
15286
|
}
|
|
14883
15287
|
async function isNewNotice(file, key) {
|
|
14884
15288
|
try {
|
|
14885
|
-
const parsed = JSON.parse(await
|
|
15289
|
+
const parsed = JSON.parse(await readFile4(file, "utf8"));
|
|
14886
15290
|
return !(isRecord4(parsed) && parsed.projectId === key.projectId && parsed.publishedVersion === key.publishedVersion && parsed.directCommitSha === key.directCommitSha && parsed.reason === key.reason);
|
|
14887
15291
|
} catch {
|
|
14888
15292
|
return true;
|
|
@@ -14890,7 +15294,7 @@ async function isNewNotice(file, key) {
|
|
|
14890
15294
|
}
|
|
14891
15295
|
async function rememberNotice(file, key) {
|
|
14892
15296
|
try {
|
|
14893
|
-
await mkdir4(
|
|
15297
|
+
await mkdir4(path19.dirname(file), { recursive: true });
|
|
14894
15298
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
14895
15299
|
const record = { v: 1, ...key };
|
|
14896
15300
|
await writeFile2(temporary, `${JSON.stringify(record)}
|
|
@@ -14919,15 +15323,15 @@ function sinceProcessStart(now2 = Date.now()) {
|
|
|
14919
15323
|
}
|
|
14920
15324
|
|
|
14921
15325
|
// src/lib/instructions/hook-trace.ts
|
|
14922
|
-
import { mkdir as mkdir5, readFile as
|
|
14923
|
-
import
|
|
15326
|
+
import { mkdir as mkdir5, readFile as readFile5, rename as rename3, writeFile as writeFile3 } from "fs/promises";
|
|
15327
|
+
import path20 from "path";
|
|
14924
15328
|
var TRACE_LIMIT = 50;
|
|
14925
15329
|
function traceFile(configDir) {
|
|
14926
|
-
return
|
|
15330
|
+
return path20.join(configDir, "traces", "instructions-hook.jsonl");
|
|
14927
15331
|
}
|
|
14928
15332
|
async function appendTrace(file, entry) {
|
|
14929
15333
|
try {
|
|
14930
|
-
const previous = await
|
|
15334
|
+
const previous = await readFile5(file, "utf8").catch(() => "");
|
|
14931
15335
|
const kept = previous.split("\n").filter((line4) => {
|
|
14932
15336
|
try {
|
|
14933
15337
|
return typeof JSON.parse(line4)?.outcome === "string";
|
|
@@ -14936,7 +15340,7 @@ async function appendTrace(file, entry) {
|
|
|
14936
15340
|
}
|
|
14937
15341
|
}).slice(-(TRACE_LIMIT - 1));
|
|
14938
15342
|
kept.push(JSON.stringify(entry));
|
|
14939
|
-
await mkdir5(
|
|
15343
|
+
await mkdir5(path20.dirname(file), { recursive: true, mode: 448 });
|
|
14940
15344
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
14941
15345
|
await writeFile3(temporary, `${kept.join("\n")}
|
|
14942
15346
|
`, { mode: 384 });
|
|
@@ -14964,7 +15368,7 @@ var Phases = class {
|
|
|
14964
15368
|
};
|
|
14965
15369
|
var show2 = (value, max = 200) => sanitizeDisplayText(value, max);
|
|
14966
15370
|
async function samePath(a, b) {
|
|
14967
|
-
const resolve = async (value) =>
|
|
15371
|
+
const resolve = async (value) => realpath5(value).catch(() => path21.resolve(value));
|
|
14968
15372
|
const [left, right] = await Promise.all([resolve(a), resolve(b)]);
|
|
14969
15373
|
return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
14970
15374
|
}
|
|
@@ -15216,11 +15620,11 @@ async function decide(run4) {
|
|
|
15216
15620
|
if (common.kind !== "ok") {
|
|
15217
15621
|
return unanswered(deadline, head, "read");
|
|
15218
15622
|
}
|
|
15219
|
-
const directory =
|
|
15623
|
+
const directory = path21.join(common.value, "fabric");
|
|
15220
15624
|
try {
|
|
15221
15625
|
await mkdir6(directory, { recursive: true });
|
|
15222
15626
|
return await withExclusiveLock(() => underLock(run4, facts), {
|
|
15223
|
-
lockPath:
|
|
15627
|
+
lockPath: path21.join(directory, "ff.lock"),
|
|
15224
15628
|
staleMs: FF_LOCK_STALE_MS,
|
|
15225
15629
|
waitMs: 0
|
|
15226
15630
|
});
|
|
@@ -15230,7 +15634,7 @@ async function decide(run4) {
|
|
|
15230
15634
|
return {
|
|
15231
15635
|
outcome: error.abandoned ? {
|
|
15232
15636
|
kind: "abandoned-lock",
|
|
15233
|
-
lockPath:
|
|
15637
|
+
lockPath: path21.join(directory, "ff.lock")
|
|
15234
15638
|
} : { kind: "locked" },
|
|
15235
15639
|
head: now2.kind === "ok" ? now2.value : head
|
|
15236
15640
|
};
|
|
@@ -15435,11 +15839,11 @@ function gatewayProbeFor(input) {
|
|
|
15435
15839
|
}
|
|
15436
15840
|
|
|
15437
15841
|
// src/lib/instructions/git-exclude.ts
|
|
15438
|
-
import { constants as
|
|
15439
|
-
import { lstat as
|
|
15440
|
-
import
|
|
15842
|
+
import { constants as constants4 } from "fs";
|
|
15843
|
+
import { lstat as lstat6, mkdir as mkdir7, open as open4 } from "fs/promises";
|
|
15844
|
+
import path22 from "path";
|
|
15441
15845
|
function relativeToTopLevel(toplevel, directory, file) {
|
|
15442
|
-
return
|
|
15846
|
+
return path22.relative(toplevel, path22.join(directory, file)).split(path22.sep).join("/");
|
|
15443
15847
|
}
|
|
15444
15848
|
function isLiteralEntry(entry) {
|
|
15445
15849
|
return !/[\n\r*?[\]\\!]/.test(entry) && !entry.startsWith("#");
|
|
@@ -15473,16 +15877,16 @@ async function excludeLocalFiles(input) {
|
|
|
15473
15877
|
if (file.kind !== "ok") {
|
|
15474
15878
|
return { kind: "failed", entries: missing };
|
|
15475
15879
|
}
|
|
15476
|
-
await mkdir7(
|
|
15477
|
-
const noFollow =
|
|
15478
|
-
const flags =
|
|
15479
|
-
const handle = await
|
|
15880
|
+
await mkdir7(path22.dirname(file.value), { recursive: true });
|
|
15881
|
+
const noFollow = constants4.O_NOFOLLOW;
|
|
15882
|
+
const flags = constants4.O_RDWR | constants4.O_APPEND | (constants4.O_NONBLOCK ?? 0) | (noFollow ?? 0);
|
|
15883
|
+
const handle = await open4(file.value, flags).catch((error) => {
|
|
15480
15884
|
if (error.code !== "ENOENT") {
|
|
15481
15885
|
throw error;
|
|
15482
15886
|
}
|
|
15483
|
-
return
|
|
15887
|
+
return open4(
|
|
15484
15888
|
file.value,
|
|
15485
|
-
flags |
|
|
15889
|
+
flags | constants4.O_CREAT | constants4.O_EXCL
|
|
15486
15890
|
);
|
|
15487
15891
|
});
|
|
15488
15892
|
try {
|
|
@@ -15491,7 +15895,7 @@ async function excludeLocalFiles(input) {
|
|
|
15491
15895
|
return { kind: "failed", entries: missing };
|
|
15492
15896
|
}
|
|
15493
15897
|
if (noFollow === void 0) {
|
|
15494
|
-
const current = await
|
|
15898
|
+
const current = await lstat6(file.value, { bigint: true });
|
|
15495
15899
|
if (!current.isFile() || current.dev !== opened.dev || current.ino !== opened.ino) {
|
|
15496
15900
|
return { kind: "failed", entries: missing };
|
|
15497
15901
|
}
|
|
@@ -15515,8 +15919,8 @@ async function excludeLocalFiles(input) {
|
|
|
15515
15919
|
}
|
|
15516
15920
|
|
|
15517
15921
|
// src/lib/instructions/hook-launcher.ts
|
|
15518
|
-
import { mkdir as mkdir8, readFile as
|
|
15519
|
-
import
|
|
15922
|
+
import { mkdir as mkdir8, readFile as readFile6, rename as rename4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
15923
|
+
import path23 from "path";
|
|
15520
15924
|
var NOT_ON_PATH_WARNING = "`fabric` is not on this machine's PATH, so this session hook will not run. Run the setup line from the project's Connect dialog instead: it keeps a copy of this deployment's CLI where the hook finds it.";
|
|
15521
15925
|
var COPY_IN_USE_WARNING = "The CLI copy the session hook runs is in use and could not be replaced, so the earlier copy was kept. Run this again when no coding session is starting.";
|
|
15522
15926
|
var UNQUOTABLE_PATH = "The CLI's config folder has characters in its path that cannot be written safely into a hook command. Set XDG_CONFIG_HOME (APPDATA on Windows) to a plain path and run this again.";
|
|
@@ -15524,7 +15928,7 @@ function deploymentFolder(origin) {
|
|
|
15524
15928
|
return origin.replace(/^(https?):\/\//, "$1-").replace(/[^A-Za-z0-9.-]+/g, "-");
|
|
15525
15929
|
}
|
|
15526
15930
|
function bundleCopyPath(configDirectory, origin) {
|
|
15527
|
-
return
|
|
15931
|
+
return path23.join(
|
|
15528
15932
|
configDirectory,
|
|
15529
15933
|
"cli",
|
|
15530
15934
|
deploymentFolder(origin),
|
|
@@ -15532,11 +15936,11 @@ function bundleCopyPath(configDirectory, origin) {
|
|
|
15532
15936
|
);
|
|
15533
15937
|
}
|
|
15534
15938
|
function samePath2(a, b) {
|
|
15535
|
-
const [left, right] = [
|
|
15939
|
+
const [left, right] = [path23.resolve(a), path23.resolve(b)];
|
|
15536
15940
|
return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
15537
15941
|
}
|
|
15538
15942
|
async function writeCopy(bytes, destination) {
|
|
15539
|
-
await mkdir8(
|
|
15943
|
+
await mkdir8(path23.dirname(destination), { recursive: true });
|
|
15540
15944
|
const temporary = `${destination}.${process.pid}.tmp`;
|
|
15541
15945
|
await writeFile4(temporary, bytes, { mode: 420 });
|
|
15542
15946
|
try {
|
|
@@ -15544,7 +15948,7 @@ async function writeCopy(bytes, destination) {
|
|
|
15544
15948
|
return true;
|
|
15545
15949
|
} catch (error) {
|
|
15546
15950
|
await rm3(temporary, { force: true });
|
|
15547
|
-
const existing = await
|
|
15951
|
+
const existing = await readFile6(destination).catch(() => null);
|
|
15548
15952
|
if (existing === null) {
|
|
15549
15953
|
throw error;
|
|
15550
15954
|
}
|
|
@@ -15567,7 +15971,7 @@ async function resolveHookLauncher(input) {
|
|
|
15567
15971
|
return { prefix: `node ${word}`, warning: null };
|
|
15568
15972
|
}
|
|
15569
15973
|
const current = await writeCopy(
|
|
15570
|
-
await
|
|
15974
|
+
await readFile6(input.bundle.scriptPath),
|
|
15571
15975
|
destination
|
|
15572
15976
|
);
|
|
15573
15977
|
return {
|
|
@@ -15620,8 +16024,8 @@ async function installHooks(input) {
|
|
|
15620
16024
|
// src/lib/instructions/lesson-prompt.ts
|
|
15621
16025
|
import { createHash as createHash11 } from "crypto";
|
|
15622
16026
|
import { createReadStream } from "fs";
|
|
15623
|
-
import { mkdir as mkdir9, readdir as readdir2, stat as
|
|
15624
|
-
import
|
|
16027
|
+
import { mkdir as mkdir9, readdir as readdir2, stat as stat4, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
|
|
16028
|
+
import path24 from "path";
|
|
15625
16029
|
import { createInterface } from "readline";
|
|
15626
16030
|
function parseStopHookInput(text3) {
|
|
15627
16031
|
let parsed;
|
|
@@ -15719,9 +16123,9 @@ async function pruneOldMarkers(markerDir, now2) {
|
|
|
15719
16123
|
const cutoff = now2.getTime() - MARKER_MAX_AGE_MS;
|
|
15720
16124
|
await Promise.all(
|
|
15721
16125
|
entries.map(async (name) => {
|
|
15722
|
-
const entryPath =
|
|
16126
|
+
const entryPath = path24.join(markerDir, name);
|
|
15723
16127
|
try {
|
|
15724
|
-
const stats = await
|
|
16128
|
+
const stats = await stat4(entryPath);
|
|
15725
16129
|
if (stats.mtimeMs < cutoff) {
|
|
15726
16130
|
await unlink2(entryPath);
|
|
15727
16131
|
}
|
|
@@ -15732,7 +16136,7 @@ async function pruneOldMarkers(markerDir, now2) {
|
|
|
15732
16136
|
}
|
|
15733
16137
|
async function markerExists(markerPath) {
|
|
15734
16138
|
try {
|
|
15735
|
-
await
|
|
16139
|
+
await stat4(markerPath);
|
|
15736
16140
|
return true;
|
|
15737
16141
|
} catch (error) {
|
|
15738
16142
|
if (error.code === "ENOENT") {
|
|
@@ -15779,7 +16183,7 @@ async function runLessonPromptInner(input) {
|
|
|
15779
16183
|
debugLog(`marker prune failed: ${describeError(error)}`);
|
|
15780
16184
|
});
|
|
15781
16185
|
const key = markerKey(parsedInput.sessionId, projectId);
|
|
15782
|
-
const markerPath =
|
|
16186
|
+
const markerPath = path24.join(input.markerDir, key);
|
|
15783
16187
|
if (await markerExists(markerPath)) {
|
|
15784
16188
|
return { output: null, why: "already-asked" };
|
|
15785
16189
|
}
|
|
@@ -15876,16 +16280,16 @@ function assertLockMatchesManifest(lock, manifest) {
|
|
|
15876
16280
|
const published = new Map(
|
|
15877
16281
|
manifest.map((entry) => [entry.path, entry.sha256])
|
|
15878
16282
|
);
|
|
15879
|
-
for (const [
|
|
15880
|
-
const expected = published.get(
|
|
16283
|
+
for (const [path42, entry] of Object.entries(lock.files)) {
|
|
16284
|
+
const expected = published.get(path42);
|
|
15881
16285
|
if (expected === void 0) {
|
|
15882
16286
|
refuseStaleLedger(
|
|
15883
|
-
`it lists ${
|
|
16287
|
+
`it lists ${path42}, which version ${lock.snapshotVersion} does not contain`
|
|
15884
16288
|
);
|
|
15885
16289
|
}
|
|
15886
16290
|
if (expected !== entry.sha256) {
|
|
15887
16291
|
refuseStaleLedger(
|
|
15888
|
-
`its record of ${
|
|
16292
|
+
`its record of ${path42} is not the published file`
|
|
15889
16293
|
);
|
|
15890
16294
|
}
|
|
15891
16295
|
}
|
|
@@ -16009,8 +16413,8 @@ async function materializePushChanges(input) {
|
|
|
16009
16413
|
`Refusing to push: ${entry.path} disappeared after planning. Run \`${fabricCommand("instructions push")}\` again.`
|
|
16010
16414
|
);
|
|
16011
16415
|
}
|
|
16012
|
-
const
|
|
16013
|
-
if (read.bytes.length !== entry.size ||
|
|
16416
|
+
const sha2562 = sha256Of(read.bytes);
|
|
16417
|
+
if (read.bytes.length !== entry.size || sha2562 !== entry.sha256) {
|
|
16014
16418
|
throw new Error(
|
|
16015
16419
|
`Refusing to push: ${entry.path} changed after planning. Run \`${fabricCommand("instructions push")}\` again.`
|
|
16016
16420
|
);
|
|
@@ -16700,7 +17104,7 @@ async function confirm(question, streams = { input: process.stdin, output: proce
|
|
|
16700
17104
|
}
|
|
16701
17105
|
|
|
16702
17106
|
// src/lib/instructions/resolve-project.ts
|
|
16703
|
-
import
|
|
17107
|
+
import path25 from "path";
|
|
16704
17108
|
var MAX_CANDIDATES = 8;
|
|
16705
17109
|
var MAX_CANDIDATE_CHARS = 512;
|
|
16706
17110
|
var MAX_LABEL_CHARS = 80;
|
|
@@ -16769,7 +17173,7 @@ async function resolveProjectFromCheckout(input) {
|
|
|
16769
17173
|
for (const match of matches2) {
|
|
16770
17174
|
if (await sameDirectory(
|
|
16771
17175
|
destination,
|
|
16772
|
-
|
|
17176
|
+
path25.join(toplevel, match.rootPath)
|
|
16773
17177
|
)) {
|
|
16774
17178
|
here.push(match);
|
|
16775
17179
|
}
|
|
@@ -16796,13 +17200,13 @@ import { spawn as spawn4 } from "child_process";
|
|
|
16796
17200
|
import { createHash as createHash14 } from "crypto";
|
|
16797
17201
|
import {
|
|
16798
17202
|
mkdir as mkdir10,
|
|
16799
|
-
readFile as
|
|
16800
|
-
realpath as
|
|
17203
|
+
readFile as readFile7,
|
|
17204
|
+
realpath as realpath6,
|
|
16801
17205
|
rm as rm4,
|
|
16802
|
-
stat as
|
|
17206
|
+
stat as stat5,
|
|
16803
17207
|
writeFile as writeFile6
|
|
16804
17208
|
} from "fs/promises";
|
|
16805
|
-
import
|
|
17209
|
+
import path26 from "path";
|
|
16806
17210
|
|
|
16807
17211
|
// src/lib/instructions/npm-tarball.ts
|
|
16808
17212
|
import { gunzipSync } from "zlib";
|
|
@@ -16936,16 +17340,16 @@ function outOfTime(input, now2) {
|
|
|
16936
17340
|
return input.deadlineAt - now2 < input.timing.budgetMs + input.timing.marginMs;
|
|
16937
17341
|
}
|
|
16938
17342
|
async function isKeptCopy(script, copy) {
|
|
16939
|
-
const actual = await
|
|
17343
|
+
const actual = await realpath6(copy).catch(() => null);
|
|
16940
17344
|
if (actual === null) {
|
|
16941
17345
|
return false;
|
|
16942
17346
|
}
|
|
16943
|
-
const running = await
|
|
17347
|
+
const running = await realpath6(script).catch(() => path26.resolve(script));
|
|
16944
17348
|
return samePath2(running, actual);
|
|
16945
17349
|
}
|
|
16946
17350
|
async function readState(file) {
|
|
16947
17351
|
try {
|
|
16948
|
-
const parsed = JSON.parse(await
|
|
17352
|
+
const parsed = JSON.parse(await readFile7(file, "utf8"));
|
|
16949
17353
|
return typeof parsed.checkedAt === "number" && Number.isFinite(parsed.checkedAt) && typeof parsed.tarball === "string" ? { checkedAt: parsed.checkedAt, tarball: parsed.tarball } : null;
|
|
16950
17354
|
} catch {
|
|
16951
17355
|
return null;
|
|
@@ -16953,7 +17357,7 @@ async function readState(file) {
|
|
|
16953
17357
|
}
|
|
16954
17358
|
async function writeState(file, state) {
|
|
16955
17359
|
try {
|
|
16956
|
-
await mkdir10(
|
|
17360
|
+
await mkdir10(path26.dirname(file), { recursive: true });
|
|
16957
17361
|
await writeFile6(file, `${JSON.stringify(state)}
|
|
16958
17362
|
`);
|
|
16959
17363
|
return true;
|
|
@@ -16963,7 +17367,7 @@ async function writeState(file, state) {
|
|
|
16963
17367
|
}
|
|
16964
17368
|
async function claimSlot(file, now2, timing) {
|
|
16965
17369
|
try {
|
|
16966
|
-
await mkdir10(
|
|
17370
|
+
await mkdir10(path26.dirname(file), { recursive: true });
|
|
16967
17371
|
for (let attempt2 = 0; attempt2 < 2; attempt2 += 1) {
|
|
16968
17372
|
try {
|
|
16969
17373
|
await writeFile6(file, `${now2}
|
|
@@ -16973,7 +17377,7 @@ async function claimSlot(file, now2, timing) {
|
|
|
16973
17377
|
if (error.code !== "EEXIST") {
|
|
16974
17378
|
return false;
|
|
16975
17379
|
}
|
|
16976
|
-
const held = await
|
|
17380
|
+
const held = await stat5(file).catch(() => null);
|
|
16977
17381
|
if (held !== null && now2 - held.mtimeMs < timing.budgetMs + timing.marginMs) {
|
|
16978
17382
|
return false;
|
|
16979
17383
|
}
|
|
@@ -17127,7 +17531,7 @@ async function refreshKeptCopy(input, deps = {}) {
|
|
|
17127
17531
|
if (!await isKeptCopy(input.script, copy)) {
|
|
17128
17532
|
return skipped2("not-a-kept-copy");
|
|
17129
17533
|
}
|
|
17130
|
-
const stateFile =
|
|
17534
|
+
const stateFile = path26.join(path26.dirname(copy), STATE_FILE);
|
|
17131
17535
|
const previous = await readState(stateFile);
|
|
17132
17536
|
if (previous !== null && askedRecently(previous, now2())) {
|
|
17133
17537
|
return skipped2("checked-recently");
|
|
@@ -17230,19 +17634,19 @@ function selfUpdateLine(outcome) {
|
|
|
17230
17634
|
}
|
|
17231
17635
|
|
|
17232
17636
|
// src/lib/instructions/tools.ts
|
|
17233
|
-
import { stat as
|
|
17234
|
-
import
|
|
17637
|
+
import { stat as stat6 } from "fs/promises";
|
|
17638
|
+
import path27 from "path";
|
|
17235
17639
|
var TOOLS = [
|
|
17236
17640
|
{ tool: "claude-code", directory: ".claude", command: "claude" },
|
|
17237
17641
|
{ tool: "codex", directory: ".codex", command: "codex" }
|
|
17238
17642
|
];
|
|
17239
17643
|
async function isDirectory(target) {
|
|
17240
|
-
return (await
|
|
17644
|
+
return (await stat6(target).catch(() => null))?.isDirectory() ?? false;
|
|
17241
17645
|
}
|
|
17242
17646
|
async function detectTools(input) {
|
|
17243
17647
|
const present = [];
|
|
17244
17648
|
for (const facts of TOOLS) {
|
|
17245
|
-
const found = await isDirectory(
|
|
17649
|
+
const found = await isDirectory(path27.join(input.root, facts.directory)) || input.home !== null && await isDirectory(path27.join(input.home, facts.directory)) || await isOnPath(facts.command, input.lookup);
|
|
17246
17650
|
if (found) {
|
|
17247
17651
|
present.push(facts.tool);
|
|
17248
17652
|
}
|
|
@@ -17251,7 +17655,7 @@ async function detectTools(input) {
|
|
|
17251
17655
|
}
|
|
17252
17656
|
|
|
17253
17657
|
// src/commands/instructions/init-plugin.ts
|
|
17254
|
-
import
|
|
17658
|
+
import path38 from "path";
|
|
17255
17659
|
|
|
17256
17660
|
// src/lib/memory/hooks.ts
|
|
17257
17661
|
var HOOK_TIMEOUT_SECONDS2 = 15;
|
|
@@ -17277,6 +17681,9 @@ function kindOf(command) {
|
|
|
17277
17681
|
if (args[0] === "instructions" && args[1] === "render") {
|
|
17278
17682
|
return "plugin-render";
|
|
17279
17683
|
}
|
|
17684
|
+
if (args[0] === "instructions" && args[1] === "rules") {
|
|
17685
|
+
return "rules-sync";
|
|
17686
|
+
}
|
|
17280
17687
|
return null;
|
|
17281
17688
|
}
|
|
17282
17689
|
function isOurs(hook, projectId, kinds) {
|
|
@@ -17332,6 +17739,18 @@ function desiredHooks(input) {
|
|
|
17332
17739
|
];
|
|
17333
17740
|
}
|
|
17334
17741
|
return [
|
|
17742
|
+
{
|
|
17743
|
+
event: "SessionStart",
|
|
17744
|
+
kind: "rules-sync",
|
|
17745
|
+
command: words(
|
|
17746
|
+
input.launcher,
|
|
17747
|
+
"instructions rules",
|
|
17748
|
+
input.projectId,
|
|
17749
|
+
binding,
|
|
17750
|
+
""
|
|
17751
|
+
),
|
|
17752
|
+
timeout: RENDER_HOOK_TIMEOUT_SECONDS
|
|
17753
|
+
},
|
|
17335
17754
|
{
|
|
17336
17755
|
event: "SessionStart",
|
|
17337
17756
|
kind: "memory-pull",
|
|
@@ -17401,7 +17820,12 @@ function without(groups, projectId, kinds) {
|
|
|
17401
17820
|
}
|
|
17402
17821
|
async function installPluginHooks(input) {
|
|
17403
17822
|
const wanted = desiredHooks(input);
|
|
17404
|
-
const allKinds = [
|
|
17823
|
+
const allKinds = [
|
|
17824
|
+
"memory-pull",
|
|
17825
|
+
"memory-push",
|
|
17826
|
+
"plugin-render",
|
|
17827
|
+
"rules-sync"
|
|
17828
|
+
];
|
|
17405
17829
|
return updateHookConfig({
|
|
17406
17830
|
root: input.root,
|
|
17407
17831
|
tool: input.tool,
|
|
@@ -17438,7 +17862,7 @@ async function installPluginHooks(input) {
|
|
|
17438
17862
|
}
|
|
17439
17863
|
|
|
17440
17864
|
// src/lib/memory/project-root.ts
|
|
17441
|
-
import
|
|
17865
|
+
import path28 from "path";
|
|
17442
17866
|
var MARKER_RELATIVE_PATH = ".fabric/project.json";
|
|
17443
17867
|
var MARKER_VERSION = 1;
|
|
17444
17868
|
var MAX_MARKER_BYTES = 64 * 1024;
|
|
@@ -17478,13 +17902,13 @@ async function writeProjectMarker(root, marker) {
|
|
|
17478
17902
|
});
|
|
17479
17903
|
}
|
|
17480
17904
|
async function findProjectRoot(start) {
|
|
17481
|
-
let current =
|
|
17905
|
+
let current = path28.resolve(start);
|
|
17482
17906
|
for (; ; ) {
|
|
17483
17907
|
const marker = await readMarker(current);
|
|
17484
17908
|
if (marker !== null) {
|
|
17485
17909
|
return { root: current, marker };
|
|
17486
17910
|
}
|
|
17487
|
-
const parent =
|
|
17911
|
+
const parent = path28.dirname(current);
|
|
17488
17912
|
if (parent === current) {
|
|
17489
17913
|
return null;
|
|
17490
17914
|
}
|
|
@@ -17492,7 +17916,7 @@ async function findProjectRoot(start) {
|
|
|
17492
17916
|
}
|
|
17493
17917
|
}
|
|
17494
17918
|
function comparable(folder) {
|
|
17495
|
-
const resolved =
|
|
17919
|
+
const resolved = path28.resolve(folder);
|
|
17496
17920
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
17497
17921
|
}
|
|
17498
17922
|
function samePath3(left, right) {
|
|
@@ -17503,7 +17927,7 @@ function gitToplevelIsTheProjects(marker, root, found) {
|
|
|
17503
17927
|
}
|
|
17504
17928
|
|
|
17505
17929
|
// src/lib/memory/runtime.ts
|
|
17506
|
-
import
|
|
17930
|
+
import path29 from "path";
|
|
17507
17931
|
var GIT_BUDGET_MS2 = 8e3;
|
|
17508
17932
|
var MemoryNotSignedIn = class extends CliFailure {
|
|
17509
17933
|
constructor(origin) {
|
|
@@ -17564,7 +17988,7 @@ function gitFor(root, marker) {
|
|
|
17564
17988
|
async function createRuntime(options, signal) {
|
|
17565
17989
|
const origin = originOf3(options.baseUrl);
|
|
17566
17990
|
const start = await resolveExistingRoot(
|
|
17567
|
-
|
|
17991
|
+
path29.resolve(options.dest ?? process.cwd())
|
|
17568
17992
|
);
|
|
17569
17993
|
const found = await findProjectRoot(start);
|
|
17570
17994
|
if (found === null) {
|
|
@@ -17604,44 +18028,95 @@ async function createRuntime(options, signal) {
|
|
|
17604
18028
|
};
|
|
17605
18029
|
}
|
|
17606
18030
|
|
|
17607
|
-
// src/lib/plugin/
|
|
17608
|
-
|
|
17609
|
-
|
|
17610
|
-
|
|
17611
|
-
|
|
17612
|
-
|
|
17613
|
-
|
|
17614
|
-
|
|
17615
|
-
|
|
17616
|
-
|
|
17617
|
-
|
|
17618
|
-
|
|
17619
|
-
|
|
17620
|
-
|
|
17621
|
-
|
|
17622
|
-
|
|
17623
|
-
|
|
17624
|
-
|
|
17625
|
-
);
|
|
17626
|
-
if (!ours) {
|
|
17627
|
-
return { kind: "none" };
|
|
18031
|
+
// src/lib/plugin/codex-install.ts
|
|
18032
|
+
import { stat as stat7 } from "fs/promises";
|
|
18033
|
+
import path32 from "path";
|
|
18034
|
+
|
|
18035
|
+
// src/lib/plugin/instruction-access.ts
|
|
18036
|
+
var INSTRUCTION_ACCESS_SKILL = "fabric-instruction-access";
|
|
18037
|
+
var INSTRUCTION_CATALOG = `skills/${INSTRUCTION_ACCESS_SKILL}/catalog.json`;
|
|
18038
|
+
function instructionAccessBootstrap(pluginName, tool) {
|
|
18039
|
+
const skill = tool === "claude-code" ? `/${pluginName}:${INSTRUCTION_ACCESS_SKILL}` : INSTRUCTION_ACCESS_SKILL;
|
|
18040
|
+
return `Fabric instruction access for plugin ${pluginName}: Load the generated ${skill} skill from this plugin before following references or choosing rules for a task. Its catalog lists every source instruction path, including rules beyond automatic delivery limits and files outside plugin folders. Read applicable rules and referenced files from their mapped local copies or this plugin's Fabric MCP server. Resolve @ references and relative paths in the original instruction tree, not by guessing paths in the user's checkout.
|
|
18041
|
+
`;
|
|
18042
|
+
}
|
|
18043
|
+
function instructionCatalog(input) {
|
|
18044
|
+
const paths = input.listed.map((entry) => entry.path);
|
|
18045
|
+
if (paths.some((entry) => !checkRelativePath(entry).ok) || findCollision(paths) !== null) {
|
|
18046
|
+
throw new Error(
|
|
18047
|
+
"The instruction catalog contains unsafe or colliding paths."
|
|
18048
|
+
);
|
|
17628
18049
|
}
|
|
17629
|
-
const
|
|
17630
|
-
|
|
17631
|
-
|
|
17632
|
-
|
|
17633
|
-
|
|
17634
|
-
|
|
18050
|
+
const entries = [...input.listed].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0).map((entry) => ({
|
|
18051
|
+
path: entry.path,
|
|
18052
|
+
size: entry.size,
|
|
18053
|
+
isText: entry.isText,
|
|
18054
|
+
kind: entry.kind,
|
|
18055
|
+
blobId: entry.blobId,
|
|
18056
|
+
sha256: entry.sha256,
|
|
18057
|
+
mode: entry.mode,
|
|
18058
|
+
localPath: input.localPaths.get(entry.path) ?? null
|
|
18059
|
+
}));
|
|
18060
|
+
return `${JSON.stringify(
|
|
18061
|
+
{
|
|
18062
|
+
version: 1,
|
|
18063
|
+
projectId: input.projectId,
|
|
18064
|
+
source: input.source,
|
|
18065
|
+
entries
|
|
18066
|
+
},
|
|
18067
|
+
null,
|
|
18068
|
+
2
|
|
18069
|
+
)}
|
|
18070
|
+
`;
|
|
18071
|
+
}
|
|
18072
|
+
function instructionAccessSkill() {
|
|
18073
|
+
return [
|
|
18074
|
+
"---",
|
|
18075
|
+
`name: ${INSTRUCTION_ACCESS_SKILL}`,
|
|
18076
|
+
"description: Discover and read this project's original coding instructions, referenced files and rules on demand, including files outside the plugin and rules beyond automatic delivery limits. Use before selecting applicable guidance or following instruction references.",
|
|
18077
|
+
"---",
|
|
18078
|
+
"",
|
|
18079
|
+
"# Fabric instruction access",
|
|
18080
|
+
"",
|
|
18081
|
+
"Search catalog.json beside this SKILL.md with your file search tool and read only matching entries, or use the MCP query below. It contains metadata for every listed instruction file, not file contents, and can be large. Do not load the whole catalog or tree into context. The catalog's projectId and source identify the instructions used to build this plugin; the plugin version is a render hash, not a repository pin.",
|
|
18082
|
+
"",
|
|
18083
|
+
"A non-null localPath is relative to this installed plugin's root (two directories above this skill folder). Read that file for the original bytes. A null localPath means the file is not stored in the plugin; automatic rule caps and unsupported layouts do not remove its catalog entry. Fabric reserves the fabric-instruction-access and fabric-living-memory skill folders; source files displaced by those names remain discoverable here and through MCP.",
|
|
18084
|
+
"",
|
|
18085
|
+
"## Select all applicable rules",
|
|
18086
|
+
"",
|
|
18087
|
+
"Automatic rule delivery can be incomplete. Find rule entries by kind and original rules/ or .claude/rules/ paths. Search the passive rule files under instructions/source/ and inspect the paths: frontmatter of each original rule to determine scope. Before affected work, read every unscoped rule and every rule whose paths match the files you will work on, including rules beyond automatic delivery limits. A filename or topic search alone cannot establish applicability. If applicable instructions cannot be read completely, report the blocked work instead of silently omitting them.",
|
|
18088
|
+
"",
|
|
18089
|
+
"## Read through this plugin's Fabric MCP server",
|
|
18090
|
+
"",
|
|
18091
|
+
"Use fabric_list_project_instructions with projectId from the catalog and query to match a path. For repository sources, also pass generation and commitSha from catalog.source. Optional prefix filters a folder; limit (1 to 500, default 200) controls the page. Read direct.generation and direct.commitSha in the response and check they match the catalog. Follow page.nextCursor by passing it as cursor with the same filters, generation and commitSha until nextCursor is null. An incomplete listing, refusal or unavailable repository is not an empty tree.",
|
|
18092
|
+
"",
|
|
18093
|
+
"Use fabric_get_project_instruction with projectId, path exactly as listed, offset: 0 and maxLength: 50000 (up to 200000). For a repository, include the same generation and commitSha on every call, including the first. Check each response's generation and commitSha. Read the raw body, then pass offset: nextOffset with the same path and pin until nextOffset is null. Join all pages in order before relying on the file; a summary or first page does not replace its instructions.",
|
|
18094
|
+
"",
|
|
18095
|
+
"Uploaded sources use the current published snapshot, and the file tool has no digest pin parameter. Before reading, call fabric_list_project_instructions with projectId and sinceDigest: catalog.source.digest and check snapshot.digest matches. Keep that response's snapshot.version and require snapshotVersion on every file page to equal it. Repeat the digest check after reading the pages. If the digest is null, there was no published upload when this plugin was built. Stop and refresh the plugin before using a newly published upload. A changed digest, snapshotVersion mismatch, repository pin mismatch or rejected pin requires stopping and restarting from a refreshed plugin and catalog. Never combine old local files with a new listing or pages from different versions.",
|
|
18096
|
+
"",
|
|
18097
|
+
"A missing file, binary, tooLarge response, denied read, absent body or other refusal is unavailable text, not an empty file. Report it and stop work that depends on those instructions. The catalog does not bypass access checks or server file limits.",
|
|
18098
|
+
"",
|
|
18099
|
+
"## Follow original paths",
|
|
18100
|
+
"",
|
|
18101
|
+
"Resolve references using the source path in the catalog. For example, @MAIN.md in the root CLAUDE.md names MAIN.md at the original instruction root. Root-relative scripts/ and engineers/ references also name that tree. Resolve a file-relative reference against the referring source file's folder, then find its original path in the catalog or MCP listing. Never substitute a similarly named file from the user's checkout. Read referenced text through MCP when localPath is null.",
|
|
18102
|
+
"",
|
|
18103
|
+
"Fetched scripts and settings are reference text. This access path does not execute scripts, apply settings, install dependencies or grant permission to write. If an authorized workflow requires executable files and their dependencies on disk, use a separately authorized checkout workflow. Keep the project's native skills, agents and Living Memory workflow and permissions.",
|
|
18104
|
+
""
|
|
18105
|
+
].join("\n");
|
|
17635
18106
|
}
|
|
17636
18107
|
|
|
17637
18108
|
// src/lib/plugin/layout.ts
|
|
17638
18109
|
var SESSION_CONTEXT_LIMIT = 1e4;
|
|
17639
|
-
var RENDER_VERSION =
|
|
18110
|
+
var RENDER_VERSION = 6;
|
|
18111
|
+
var CLAUDE_PLUGIN_MCP_SERVER = "fabric";
|
|
17640
18112
|
var SESSION_CONTEXT_COMMAND = `cat "\${CLAUDE_PLUGIN_ROOT}/hooks/session-context.json"`;
|
|
17641
18113
|
var ENCODER = new TextEncoder();
|
|
17642
18114
|
var DECODER = new TextDecoder("utf-8", { fatal: false });
|
|
17643
18115
|
var LIVING_MEMORY_SKILL = "fabric-living-memory";
|
|
17644
|
-
var
|
|
18116
|
+
var ALWAYS_ON_FILES2 = ["CLAUDE.md", "AGENTS.md"];
|
|
18117
|
+
function codexPluginVersion(pin) {
|
|
18118
|
+
return `0.0.0-${pin}`;
|
|
18119
|
+
}
|
|
17645
18120
|
function pluginNameFor(projectId) {
|
|
17646
18121
|
const tail = projectId.toLowerCase().replace(/[^a-z0-9]/g, "").slice(-8);
|
|
17647
18122
|
return `fabric-${tail === "" ? "project" : tail}`;
|
|
@@ -17658,28 +18133,8 @@ function isExecutable(mode) {
|
|
|
17658
18133
|
function fileModeOf(mode) {
|
|
17659
18134
|
return isExecutable(mode) ? 493 : 420;
|
|
17660
18135
|
}
|
|
17661
|
-
function pluginPathFor(instructionPath) {
|
|
17662
|
-
const segments = instructionPath.split("/");
|
|
17663
|
-
const first = segments[0]?.toLowerCase();
|
|
17664
|
-
const rest = first === ".claude" ? segments.slice(1) : segments;
|
|
17665
|
-
const folder = rest[0]?.toLowerCase();
|
|
17666
|
-
if (rest.length < 2) {
|
|
17667
|
-
return null;
|
|
17668
|
-
}
|
|
17669
|
-
const below = rest.slice(1);
|
|
17670
|
-
switch (folder) {
|
|
17671
|
-
case "skills":
|
|
17672
|
-
return below.length >= 2 ? `skills/${below.join("/")}` : null;
|
|
17673
|
-
case "agents":
|
|
17674
|
-
return below.length === 1 && below[0]?.toLowerCase().endsWith(".md") ? `agents/${below[0]}` : null;
|
|
17675
|
-
case "commands":
|
|
17676
|
-
return `commands/${below.join("/")}`;
|
|
17677
|
-
default:
|
|
17678
|
-
return null;
|
|
17679
|
-
}
|
|
17680
|
-
}
|
|
17681
18136
|
function alwaysOnFile(files, tool = "claude-code") {
|
|
17682
|
-
const order = tool === "codex" ? ["AGENTS.md", "CLAUDE.md"] :
|
|
18137
|
+
const order = tool === "codex" ? ["AGENTS.md", "CLAUDE.md"] : ALWAYS_ON_FILES2;
|
|
17683
18138
|
for (const name of order) {
|
|
17684
18139
|
const found = files.find((entry) => entry.path === name);
|
|
17685
18140
|
if (found !== void 0) {
|
|
@@ -17696,8 +18151,8 @@ function truncationNotice(file, pluginName) {
|
|
|
17696
18151
|
|
|
17697
18152
|
[Fabric: this file is longer than the ${SESSION_CONTEXT_LIMIT.toLocaleString("en-US")} characters a session start can carry, so it was cut here. Read the rest in full before relying on it: instructions/${file} inside the ${pluginName} plugin, for example ~/.claude/plugins/cache/${pluginName}/${pluginName}/*/instructions/${file}]`;
|
|
17698
18153
|
}
|
|
17699
|
-
function sessionContext(file, text3, pluginName) {
|
|
17700
|
-
const heading =
|
|
18154
|
+
function sessionContext(file, text3, pluginName, guidance = "") {
|
|
18155
|
+
const heading = `${guidance}${guidance === "" ? "" : "\n"}Coding instructions for this project (${file}, from Fabric):
|
|
17701
18156
|
|
|
17702
18157
|
`;
|
|
17703
18158
|
const whole = heading + text3;
|
|
@@ -17726,7 +18181,7 @@ function folderLine(folder) {
|
|
|
17726
18181
|
return `- \`${folder.folder}/\` -- ${where}`;
|
|
17727
18182
|
}
|
|
17728
18183
|
function livingMemorySkill(memory, tool) {
|
|
17729
|
-
const delivery = tool === "claude-code" ? "Fabric delivers them: a hook sends new and changed files when your turn ends, and brings other people's changes in when a session starts. Do not run git for these files." : "Fabric delivers them when the next session starts (this tool has no end-of-turn hook).
|
|
18184
|
+
const delivery = tool === "claude-code" ? "Fabric delivers them: a hook sends new and changed files when your turn ends, and brings other people's changes in when a session starts. Do not run git for these files." : "Fabric delivers them when the next session starts (this tool has no end-of-turn hook). Do not run git for these files.";
|
|
17730
18185
|
const header = [
|
|
17731
18186
|
"---",
|
|
17732
18187
|
`name: ${LIVING_MEMORY_SKILL}`,
|
|
@@ -17739,7 +18194,7 @@ function livingMemorySkill(memory, tool) {
|
|
|
17739
18194
|
let body;
|
|
17740
18195
|
if (memory.state === "unavailable") {
|
|
17741
18196
|
body = [
|
|
17742
|
-
"The list of this project's memory folders could not be read when this plugin was built.
|
|
18197
|
+
"The list of this project's memory folders could not be read when this plugin was built. Call the `fabric_memory_targets` tool of the Fabric MCP server to see them, and do not save memory files until you know the folder."
|
|
17743
18198
|
];
|
|
17744
18199
|
} else if (memory.folders.length === 0) {
|
|
17745
18200
|
body = [
|
|
@@ -17753,105 +18208,137 @@ function livingMemorySkill(memory, tool) {
|
|
|
17753
18208
|
"",
|
|
17754
18209
|
delivery,
|
|
17755
18210
|
"",
|
|
17756
|
-
"Never save memory files anywhere else. A file in a pull-request folder is not part of the project's Living Memory until the pull request is merged."
|
|
18211
|
+
"Never save memory files anywhere else. A file in a pull-request folder is not part of the project's Living Memory until the pull request is merged.",
|
|
18212
|
+
"",
|
|
18213
|
+
"Keep one work item's specs and tasks together in `docs/ai/specs/<work-item-id>/`, inside one of the folders above. To resume a work item, list that folder with the `fabric_memory_read` tool (pass `prefix`, then a file's `path` to read it), or read the local mirror: the same files, at the same paths, in the project folder.",
|
|
18214
|
+
"",
|
|
18215
|
+
"To check a delivery, read the `writeId` of each entry in the `outbox` of `.fabric/memory.lock` and pass it to the `fabric_memory_write_status` tool."
|
|
17757
18216
|
];
|
|
17758
18217
|
}
|
|
17759
18218
|
return `${[...header, ...body].join("\n")}
|
|
17760
18219
|
`;
|
|
17761
18220
|
}
|
|
17762
|
-
function descriptionFor() {
|
|
17763
|
-
|
|
18221
|
+
function descriptionFor(projectId, gatewayUrl) {
|
|
18222
|
+
let host = gatewayUrl;
|
|
18223
|
+
try {
|
|
18224
|
+
host = new URL(gatewayUrl).host;
|
|
18225
|
+
} catch {
|
|
18226
|
+
}
|
|
18227
|
+
return `Coding instructions for Fabric project ${projectId} on ${host}, rendered from its published instructions.`;
|
|
17764
18228
|
}
|
|
17765
|
-
function mcpConfiguration(
|
|
17766
|
-
return json({
|
|
17767
|
-
mcpServers: {
|
|
17768
|
-
[serverName]: {
|
|
17769
|
-
type: tool === "codex" ? "streamable-http" : "http",
|
|
17770
|
-
url
|
|
17771
|
-
}
|
|
17772
|
-
}
|
|
17773
|
-
});
|
|
18229
|
+
function mcpConfiguration(serverName, url) {
|
|
18230
|
+
return json({ mcpServers: { [serverName]: { type: "http", url } } });
|
|
17774
18231
|
}
|
|
17775
18232
|
function buildPlugin(inputs) {
|
|
17776
18233
|
const name = pluginNameFor(inputs.projectId);
|
|
17777
18234
|
const out = /* @__PURE__ */ new Map();
|
|
17778
|
-
const put = (
|
|
17779
|
-
out.set(
|
|
18235
|
+
const put = (path42, bytes, mode = 420) => {
|
|
18236
|
+
out.set(path42, { path: path42, bytes, mode });
|
|
17780
18237
|
};
|
|
18238
|
+
const sourceAtTarget = /* @__PURE__ */ new Map();
|
|
17781
18239
|
for (const file of [...inputs.files].sort(
|
|
17782
18240
|
(a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
|
|
17783
18241
|
)) {
|
|
17784
|
-
const
|
|
17785
|
-
|
|
17786
|
-
|
|
18242
|
+
const nativeTarget = pluginPathFor(file.path);
|
|
18243
|
+
const target = file.path === "CLAUDE.md" || file.path === "AGENTS.md" ? `instructions/${file.path}` : projectRulePathFor(file.path) !== null ? `instructions/source/${file.path}` : nativeTarget;
|
|
18244
|
+
if (target === null || [LIVING_MEMORY_SKILL, INSTRUCTION_ACCESS_SKILL].some(
|
|
18245
|
+
(reserved) => target.toLowerCase().startsWith(`skills/${reserved}/`)
|
|
18246
|
+
)) {
|
|
18247
|
+
continue;
|
|
17787
18248
|
}
|
|
18249
|
+
put(
|
|
18250
|
+
target,
|
|
18251
|
+
file.bytes,
|
|
18252
|
+
nativeTarget === null ? 420 : fileModeOf(file.mode)
|
|
18253
|
+
);
|
|
18254
|
+
sourceAtTarget.set(target, file.path);
|
|
17788
18255
|
}
|
|
18256
|
+
const localPaths = new Map(
|
|
18257
|
+
[...sourceAtTarget].map(([target, source]) => [source, target])
|
|
18258
|
+
);
|
|
17789
18259
|
put(
|
|
17790
18260
|
`skills/${LIVING_MEMORY_SKILL}/SKILL.md`,
|
|
17791
18261
|
bytesOf(livingMemorySkill(inputs.memory, inputs.tool))
|
|
17792
18262
|
);
|
|
17793
|
-
|
|
17794
|
-
|
|
17795
|
-
|
|
17796
|
-
|
|
17797
|
-
|
|
18263
|
+
put(
|
|
18264
|
+
`skills/${INSTRUCTION_ACCESS_SKILL}/SKILL.md`,
|
|
18265
|
+
bytesOf(instructionAccessSkill())
|
|
18266
|
+
);
|
|
18267
|
+
put(
|
|
18268
|
+
INSTRUCTION_CATALOG,
|
|
18269
|
+
bytesOf(
|
|
18270
|
+
instructionCatalog({
|
|
18271
|
+
projectId: inputs.projectId,
|
|
18272
|
+
source: inputs.instructionAccess.source,
|
|
18273
|
+
listed: inputs.instructionAccess.listed,
|
|
18274
|
+
localPaths
|
|
18275
|
+
})
|
|
18276
|
+
)
|
|
18277
|
+
);
|
|
17798
18278
|
if (inputs.tool === "claude-code") {
|
|
17799
18279
|
put(
|
|
17800
18280
|
".claude-plugin/plugin.json",
|
|
17801
18281
|
json({
|
|
17802
18282
|
name,
|
|
17803
|
-
description: descriptionFor(
|
|
18283
|
+
description: descriptionFor(
|
|
18284
|
+
inputs.projectId,
|
|
18285
|
+
inputs.gatewayUrl
|
|
18286
|
+
),
|
|
17804
18287
|
author: { name: "Fabric" }
|
|
17805
18288
|
})
|
|
17806
18289
|
);
|
|
17807
18290
|
put(
|
|
17808
18291
|
".mcp.json",
|
|
17809
|
-
mcpConfiguration(
|
|
18292
|
+
mcpConfiguration(CLAUDE_PLUGIN_MCP_SERVER, inputs.gatewayUrl)
|
|
17810
18293
|
);
|
|
17811
18294
|
const always = alwaysOnFile(inputs.files);
|
|
17812
|
-
|
|
17813
|
-
|
|
17814
|
-
|
|
17815
|
-
|
|
17816
|
-
|
|
17817
|
-
|
|
17818
|
-
|
|
17819
|
-
|
|
17820
|
-
|
|
17821
|
-
|
|
17822
|
-
|
|
17823
|
-
|
|
17824
|
-
|
|
17825
|
-
|
|
17826
|
-
|
|
17827
|
-
|
|
17828
|
-
|
|
17829
|
-
|
|
17830
|
-
|
|
17831
|
-
|
|
17832
|
-
|
|
17833
|
-
|
|
17834
|
-
|
|
17835
|
-
|
|
17836
|
-
|
|
17837
|
-
|
|
17838
|
-
|
|
17839
|
-
|
|
17840
|
-
|
|
17841
|
-
|
|
17842
|
-
|
|
17843
|
-
|
|
17844
|
-
}
|
|
18295
|
+
const guidance = instructionAccessBootstrap(name, inputs.tool);
|
|
18296
|
+
put(
|
|
18297
|
+
"hooks/session-context.json",
|
|
18298
|
+
json({
|
|
18299
|
+
hookSpecificOutput: {
|
|
18300
|
+
hookEventName: "SessionStart",
|
|
18301
|
+
additionalContext: always === null ? guidance : sessionContext(
|
|
18302
|
+
always.file,
|
|
18303
|
+
always.text,
|
|
18304
|
+
name,
|
|
18305
|
+
guidance
|
|
18306
|
+
)
|
|
18307
|
+
}
|
|
18308
|
+
})
|
|
18309
|
+
);
|
|
18310
|
+
put(
|
|
18311
|
+
"hooks/hooks.json",
|
|
18312
|
+
json({
|
|
18313
|
+
hooks: {
|
|
18314
|
+
SessionStart: [
|
|
18315
|
+
{
|
|
18316
|
+
hooks: [
|
|
18317
|
+
{
|
|
18318
|
+
type: "command",
|
|
18319
|
+
command: SESSION_CONTEXT_COMMAND
|
|
18320
|
+
}
|
|
18321
|
+
]
|
|
18322
|
+
}
|
|
18323
|
+
]
|
|
18324
|
+
}
|
|
18325
|
+
})
|
|
18326
|
+
);
|
|
17845
18327
|
} else {
|
|
17846
18328
|
put(
|
|
17847
|
-
"plugin.json",
|
|
18329
|
+
".codex-plugin/plugin.json",
|
|
17848
18330
|
json({
|
|
17849
18331
|
name,
|
|
17850
|
-
version:
|
|
17851
|
-
description: descriptionFor(
|
|
18332
|
+
version: codexPluginVersion(inputs.pin),
|
|
18333
|
+
description: descriptionFor(
|
|
18334
|
+
inputs.projectId,
|
|
18335
|
+
inputs.gatewayUrl
|
|
18336
|
+
),
|
|
18337
|
+
skills: "./skills/",
|
|
18338
|
+
mcpServers: "./.mcp.json"
|
|
17852
18339
|
})
|
|
17853
18340
|
);
|
|
17854
|
-
put("mcp.json", mcpConfiguration(
|
|
18341
|
+
put(".mcp.json", mcpConfiguration(name, inputs.gatewayUrl));
|
|
17855
18342
|
}
|
|
17856
18343
|
return [...out.values()].sort(
|
|
17857
18344
|
(a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
|
|
@@ -17875,8 +18362,113 @@ function isFabricAgentsOverride(text3) {
|
|
|
17875
18362
|
}
|
|
17876
18363
|
|
|
17877
18364
|
// src/lib/plugin/register.ts
|
|
17878
|
-
import { mkdir as mkdir11, readFile as
|
|
18365
|
+
import { mkdir as mkdir11, readFile as readFile9, rename as rename5, rm as rm5, writeFile as writeFile7 } from "fs/promises";
|
|
17879
18366
|
import path31 from "path";
|
|
18367
|
+
|
|
18368
|
+
// src/lib/plugin/binding.ts
|
|
18369
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
18370
|
+
import path30 from "path";
|
|
18371
|
+
var BINDING_FILE = ".fabric-binding.json";
|
|
18372
|
+
function isRecord6(value) {
|
|
18373
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18374
|
+
}
|
|
18375
|
+
function originOf4(value) {
|
|
18376
|
+
try {
|
|
18377
|
+
return new URL(value).origin;
|
|
18378
|
+
} catch {
|
|
18379
|
+
return null;
|
|
18380
|
+
}
|
|
18381
|
+
}
|
|
18382
|
+
function bindingText(binding) {
|
|
18383
|
+
return `${JSON.stringify(binding, null, 2)}
|
|
18384
|
+
`;
|
|
18385
|
+
}
|
|
18386
|
+
async function readJson(file) {
|
|
18387
|
+
try {
|
|
18388
|
+
return JSON.parse(await readFile8(file, "utf8"));
|
|
18389
|
+
} catch {
|
|
18390
|
+
return null;
|
|
18391
|
+
}
|
|
18392
|
+
}
|
|
18393
|
+
function valueAfter(words2, flag) {
|
|
18394
|
+
const at = words2.indexOf(flag);
|
|
18395
|
+
return at >= 0 ? words2[at + 1] ?? null : null;
|
|
18396
|
+
}
|
|
18397
|
+
async function bindingFromSidecar(directory) {
|
|
18398
|
+
const parsed = await readJson(path30.join(directory, BINDING_FILE));
|
|
18399
|
+
if (!isRecord6(parsed) || typeof parsed.projectId !== "string") {
|
|
18400
|
+
return null;
|
|
18401
|
+
}
|
|
18402
|
+
return {
|
|
18403
|
+
projectId: parsed.projectId,
|
|
18404
|
+
origin: typeof parsed.origin === "string" ? parsed.origin : null
|
|
18405
|
+
};
|
|
18406
|
+
}
|
|
18407
|
+
async function legacyClaudeBinding(directory) {
|
|
18408
|
+
const parsed = await readJson(
|
|
18409
|
+
path30.join(directory, ".claude-plugin", "marketplace.json")
|
|
18410
|
+
);
|
|
18411
|
+
if (!isRecord6(parsed) || !Array.isArray(parsed.plugins)) {
|
|
18412
|
+
return null;
|
|
18413
|
+
}
|
|
18414
|
+
for (const plugin of parsed.plugins) {
|
|
18415
|
+
const source = isRecord6(plugin) ? plugin.source : null;
|
|
18416
|
+
const command = isRecord6(source) ? source.command : null;
|
|
18417
|
+
if (typeof command !== "string") {
|
|
18418
|
+
continue;
|
|
18419
|
+
}
|
|
18420
|
+
const words2 = command.split(/\s+/);
|
|
18421
|
+
const projectId = valueAfter(words2, "--project");
|
|
18422
|
+
if (projectId !== null) {
|
|
18423
|
+
const baseUrl = valueAfter(words2, "--base-url");
|
|
18424
|
+
return {
|
|
18425
|
+
projectId,
|
|
18426
|
+
origin: baseUrl === null ? null : originOf4(baseUrl)
|
|
18427
|
+
};
|
|
18428
|
+
}
|
|
18429
|
+
}
|
|
18430
|
+
return null;
|
|
18431
|
+
}
|
|
18432
|
+
var GATEWAY_URL = /^(.+)\/api\/mcp-gateway\/projects\/([A-Za-z0-9_-]+)\/?$/;
|
|
18433
|
+
async function legacyCodexBinding(directory) {
|
|
18434
|
+
const parsed = await readJson(path30.join(directory, ".mcp.json"));
|
|
18435
|
+
if (!isRecord6(parsed) || !isRecord6(parsed.mcpServers)) {
|
|
18436
|
+
return null;
|
|
18437
|
+
}
|
|
18438
|
+
for (const server of Object.values(parsed.mcpServers)) {
|
|
18439
|
+
const url = isRecord6(server) ? server.url : null;
|
|
18440
|
+
if (typeof url !== "string") {
|
|
18441
|
+
continue;
|
|
18442
|
+
}
|
|
18443
|
+
const projectId = GATEWAY_URL.exec(url)?.[2];
|
|
18444
|
+
if (projectId !== void 0) {
|
|
18445
|
+
return { projectId, origin: originOf4(url) };
|
|
18446
|
+
}
|
|
18447
|
+
}
|
|
18448
|
+
return null;
|
|
18449
|
+
}
|
|
18450
|
+
async function readBinding(directory, location) {
|
|
18451
|
+
return await bindingFromSidecar(directory) ?? (location === "claude-marketplace" ? await legacyClaudeBinding(directory) : await legacyCodexBinding(directory));
|
|
18452
|
+
}
|
|
18453
|
+
async function assertLocationFree(input) {
|
|
18454
|
+
const existing = await readBinding(input.directory, input.location);
|
|
18455
|
+
if (existing === null) {
|
|
18456
|
+
return;
|
|
18457
|
+
}
|
|
18458
|
+
const otherProject = existing.projectId !== input.wanted.projectId;
|
|
18459
|
+
const otherDeployment = existing.origin !== null && input.wanted.origin !== null && existing.origin !== input.wanted.origin;
|
|
18460
|
+
if (!otherProject && !otherDeployment) {
|
|
18461
|
+
return;
|
|
18462
|
+
}
|
|
18463
|
+
const name = pluginNameFor(input.wanted.projectId);
|
|
18464
|
+
const removal = input.location === "claude-marketplace" ? `run \`claude plugin uninstall ${name}@${name} --scope local\` in the folders that use it, then delete ${input.directory}` : `delete ${input.directory} and remove ${name} from the Codex marketplace file that lists it`;
|
|
18465
|
+
throw new CliFailure(
|
|
18466
|
+
`The plugin name ${name} is already taken by project ${existing.projectId}${existing.origin === null ? "" : ` on ${existing.origin}`}, so it was not changed for project ${input.wanted.projectId}${input.wanted.origin === null ? "" : ` on ${input.wanted.origin}`}. Replacing it would repoint the folders already set up for that project. To use this one, remove the other setup first: ${removal}, then run this again.`,
|
|
18467
|
+
7
|
|
18468
|
+
);
|
|
18469
|
+
}
|
|
18470
|
+
|
|
18471
|
+
// src/lib/plugin/register.ts
|
|
17880
18472
|
var COMMAND_MAX_LENGTH = 500;
|
|
17881
18473
|
var RENDER_TIMEOUT_SECONDS = 120;
|
|
17882
18474
|
function marketplaceNameFor(projectId) {
|
|
@@ -17936,7 +18528,7 @@ function claudeMarketplace(input) {
|
|
|
17936
18528
|
plugins: [
|
|
17937
18529
|
{
|
|
17938
18530
|
name: pluginNameFor(input.projectId),
|
|
17939
|
-
description:
|
|
18531
|
+
description: descriptionFor(input.projectId, input.origin),
|
|
17940
18532
|
source: {
|
|
17941
18533
|
source: "command",
|
|
17942
18534
|
command: input.command,
|
|
@@ -17952,6 +18544,19 @@ async function writeClaudeMarketplace(input) {
|
|
|
17952
18544
|
input.cacheDirectory,
|
|
17953
18545
|
input.projectId
|
|
17954
18546
|
);
|
|
18547
|
+
const binding = {
|
|
18548
|
+
projectId: input.projectId,
|
|
18549
|
+
origin: originOf4(input.origin)
|
|
18550
|
+
};
|
|
18551
|
+
await assertLocationFree({
|
|
18552
|
+
directory,
|
|
18553
|
+
location: "claude-marketplace",
|
|
18554
|
+
wanted: binding
|
|
18555
|
+
});
|
|
18556
|
+
await writeAtomically(
|
|
18557
|
+
path31.join(directory, BINDING_FILE),
|
|
18558
|
+
bindingText(binding)
|
|
18559
|
+
);
|
|
17955
18560
|
await writeAtomically(
|
|
17956
18561
|
path31.join(directory, ".claude-plugin", "marketplace.json"),
|
|
17957
18562
|
`${JSON.stringify(claudeMarketplace(input), null, 2)}
|
|
@@ -17998,7 +18603,7 @@ var CODEX_MARKETPLACE_RELATIVE = path31.join(
|
|
|
17998
18603
|
"marketplace.json"
|
|
17999
18604
|
);
|
|
18000
18605
|
var CODEX_MARKETPLACE_NAME = "personal";
|
|
18001
|
-
function
|
|
18606
|
+
function isRecord7(value) {
|
|
18002
18607
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18003
18608
|
}
|
|
18004
18609
|
function withCodexPlugin(marketplace, input) {
|
|
@@ -18016,7 +18621,7 @@ function withCodexPlugin(marketplace, input) {
|
|
|
18016
18621
|
async function readCodexMarketplace(file) {
|
|
18017
18622
|
let raw;
|
|
18018
18623
|
try {
|
|
18019
|
-
raw = await
|
|
18624
|
+
raw = await readFile9(file, "utf8");
|
|
18020
18625
|
} catch (error) {
|
|
18021
18626
|
if (error.code === "ENOENT") {
|
|
18022
18627
|
return {
|
|
@@ -18039,7 +18644,7 @@ async function readCodexMarketplace(file) {
|
|
|
18039
18644
|
7
|
|
18040
18645
|
);
|
|
18041
18646
|
}
|
|
18042
|
-
if (!
|
|
18647
|
+
if (!isRecord7(parsed) || typeof parsed.name !== "string" || !Array.isArray(parsed.plugins) || !parsed.plugins.every(isRecord7)) {
|
|
18043
18648
|
throw new CliFailure(
|
|
18044
18649
|
`${file} does not look like a plugin marketplace (a "name" and a "plugins" list). It was left untouched.`,
|
|
18045
18650
|
7
|
|
@@ -18068,7 +18673,7 @@ async function registerCodexPlugin(input) {
|
|
|
18068
18673
|
});
|
|
18069
18674
|
const before = JSON.stringify(marketplace);
|
|
18070
18675
|
const changed = JSON.stringify(next) !== before;
|
|
18071
|
-
if (changed || !await
|
|
18676
|
+
if (changed || !await readFile9(file).then(
|
|
18072
18677
|
() => true,
|
|
18073
18678
|
() => false
|
|
18074
18679
|
)) {
|
|
@@ -18081,13 +18686,24 @@ async function registerCodexPlugin(input) {
|
|
|
18081
18686
|
changed
|
|
18082
18687
|
};
|
|
18083
18688
|
}
|
|
18689
|
+
async function codexMarketplaceListing(input) {
|
|
18690
|
+
try {
|
|
18691
|
+
const { marketplace, existed } = await readCodexMarketplace(
|
|
18692
|
+
path31.join(input.home, CODEX_MARKETPLACE_RELATIVE)
|
|
18693
|
+
);
|
|
18694
|
+
const name = pluginNameFor(input.projectId);
|
|
18695
|
+
return existed && marketplace.plugins.some((plugin) => plugin.name === name) ? marketplace.name : null;
|
|
18696
|
+
} catch {
|
|
18697
|
+
return null;
|
|
18698
|
+
}
|
|
18699
|
+
}
|
|
18084
18700
|
async function writeAgentsOverride(input) {
|
|
18085
18701
|
const file = path31.join(input.root, "AGENTS.override.md");
|
|
18086
|
-
const existing = await
|
|
18702
|
+
const existing = await readFile9(file, "utf8").catch(() => null);
|
|
18087
18703
|
if (existing !== null && !isFabricAgentsOverride(existing)) {
|
|
18088
18704
|
return { kind: "left-alone", file };
|
|
18089
18705
|
}
|
|
18090
|
-
const tracked = await
|
|
18706
|
+
const tracked = await readFile9(
|
|
18091
18707
|
path31.join(input.root, "AGENTS.md"),
|
|
18092
18708
|
"utf8"
|
|
18093
18709
|
).catch(() => null);
|
|
@@ -18102,31 +18718,467 @@ async function writeAgentsOverride(input) {
|
|
|
18102
18718
|
return { kind: "written", file };
|
|
18103
18719
|
}
|
|
18104
18720
|
|
|
18105
|
-
// src/
|
|
18106
|
-
|
|
18107
|
-
|
|
18721
|
+
// src/lib/plugin/codex-install.ts
|
|
18722
|
+
var INSTALL_TIMEOUT_MS = 12e4;
|
|
18723
|
+
var REFRESH_TIMEOUT_MS = 25e3;
|
|
18724
|
+
function codexPluginReference(projectId, marketplaceName) {
|
|
18725
|
+
return `${pluginNameFor(projectId)}@${marketplaceName}`;
|
|
18726
|
+
}
|
|
18727
|
+
async function installCodexPlugin(input) {
|
|
18728
|
+
const args = [
|
|
18729
|
+
"plugin",
|
|
18730
|
+
"add",
|
|
18731
|
+
codexPluginReference(input.projectId, input.marketplaceName)
|
|
18732
|
+
];
|
|
18733
|
+
const result = await input.run("codex", args, {
|
|
18734
|
+
cwd: input.cwd,
|
|
18735
|
+
timeoutMs: INSTALL_TIMEOUT_MS
|
|
18736
|
+
});
|
|
18737
|
+
return result.kind === "exited" && result.code === 0 ? { kind: "installed" } : { kind: "manual", line: pasteableLine(["codex", ...args]) };
|
|
18738
|
+
}
|
|
18739
|
+
async function refreshCodexInstall(input) {
|
|
18740
|
+
try {
|
|
18741
|
+
const marketplaceName = await codexMarketplaceListing(input);
|
|
18742
|
+
if (marketplaceName === null) {
|
|
18743
|
+
return;
|
|
18744
|
+
}
|
|
18745
|
+
const installed = path32.join(
|
|
18746
|
+
input.codexHome,
|
|
18747
|
+
"plugins",
|
|
18748
|
+
"cache",
|
|
18749
|
+
marketplaceName,
|
|
18750
|
+
pluginNameFor(input.projectId),
|
|
18751
|
+
input.version
|
|
18752
|
+
);
|
|
18753
|
+
if (await stat7(installed).then(
|
|
18754
|
+
(found) => found.isDirectory(),
|
|
18755
|
+
() => false
|
|
18756
|
+
)) {
|
|
18757
|
+
return;
|
|
18758
|
+
}
|
|
18759
|
+
await input.run(
|
|
18760
|
+
"codex",
|
|
18761
|
+
[
|
|
18762
|
+
"plugin",
|
|
18763
|
+
"add",
|
|
18764
|
+
codexPluginReference(input.projectId, marketplaceName)
|
|
18765
|
+
],
|
|
18766
|
+
{ cwd: input.cwd, timeoutMs: REFRESH_TIMEOUT_MS }
|
|
18767
|
+
);
|
|
18768
|
+
} catch {
|
|
18769
|
+
}
|
|
18770
|
+
}
|
|
18771
|
+
|
|
18772
|
+
// src/lib/plugin/gateway.ts
|
|
18773
|
+
var REMOVE_TIMEOUT_MS = 3e4;
|
|
18774
|
+
async function removeLocalClaudeGateway(input) {
|
|
18775
|
+
const name = serverNameFor(input.projectId);
|
|
18776
|
+
const inspected = await inspectServers({
|
|
18777
|
+
tool: "claude-code",
|
|
18778
|
+
origin: input.origin,
|
|
18779
|
+
projectId: input.projectId,
|
|
18780
|
+
cwd: input.cwd,
|
|
18781
|
+
home: input.home,
|
|
18782
|
+
env: input.env,
|
|
18783
|
+
platform: input.platform
|
|
18784
|
+
});
|
|
18785
|
+
if (inspected.state !== "read") {
|
|
18786
|
+
return { kind: "unchecked" };
|
|
18787
|
+
}
|
|
18788
|
+
const ours = inspected.servers.some(
|
|
18789
|
+
(server) => server.scope === "local" && server.name === name && server.kind === "this-project"
|
|
18790
|
+
);
|
|
18791
|
+
if (!ours) {
|
|
18792
|
+
return { kind: "none" };
|
|
18793
|
+
}
|
|
18794
|
+
const removed = await input.run(
|
|
18795
|
+
"claude",
|
|
18796
|
+
["mcp", "remove", name, "--scope", "local"],
|
|
18797
|
+
{ cwd: input.cwd, timeoutMs: REMOVE_TIMEOUT_MS }
|
|
18798
|
+
);
|
|
18799
|
+
return removed.kind === "exited" && removed.code === 0 ? { kind: "removed", name } : { kind: "failed", name };
|
|
18800
|
+
}
|
|
18801
|
+
|
|
18802
|
+
// src/lib/plugin/rules-sync.ts
|
|
18803
|
+
import { createHash as createHash15 } from "crypto";
|
|
18804
|
+
import { rmdir as rmdir2 } from "fs/promises";
|
|
18805
|
+
import path33 from "path";
|
|
18806
|
+
|
|
18807
|
+
// src/lib/plugin/rules.ts
|
|
18808
|
+
var RULES_MAX_FILES = 64;
|
|
18809
|
+
var RULES_MAX_BYTES = 256 * 1024;
|
|
18810
|
+
var AGENTS_RULES_LIMIT = 24e3;
|
|
18811
|
+
var byPath4 = (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
|
|
18812
|
+
function selectRules(read, tool) {
|
|
18813
|
+
const bytesOf2 = new Map(read.files.map((file) => [file.path, file.bytes]));
|
|
18814
|
+
const seen = /* @__PURE__ */ new Set();
|
|
18815
|
+
const rules = [];
|
|
18816
|
+
for (const rule of [...planPluginFiles(read.listed, tool).rules].sort(
|
|
18817
|
+
byPath4
|
|
18818
|
+
)) {
|
|
18819
|
+
const bytes = bytesOf2.get(rule.path);
|
|
18820
|
+
if (bytes === void 0 || seen.has(rule.as)) {
|
|
18821
|
+
continue;
|
|
18822
|
+
}
|
|
18823
|
+
seen.add(rule.as);
|
|
18824
|
+
rules.push({ path: rule.as, source: rule.path, bytes });
|
|
18825
|
+
}
|
|
18826
|
+
return rules.sort(byPath4);
|
|
18827
|
+
}
|
|
18828
|
+
function capRules(rules) {
|
|
18829
|
+
const kept = [];
|
|
18830
|
+
let total = 0;
|
|
18831
|
+
for (const rule of rules) {
|
|
18832
|
+
if (kept.length >= RULES_MAX_FILES || total + rule.bytes.byteLength > RULES_MAX_BYTES) {
|
|
18833
|
+
break;
|
|
18834
|
+
}
|
|
18835
|
+
kept.push(rule);
|
|
18836
|
+
total += rule.bytes.byteLength;
|
|
18837
|
+
}
|
|
18838
|
+
return { kept, omitted: rules.length - kept.length };
|
|
18839
|
+
}
|
|
18840
|
+
var DECODER2 = new TextDecoder("utf-8", { fatal: false });
|
|
18841
|
+
function unquote(value) {
|
|
18842
|
+
const trimmed = value.trim();
|
|
18843
|
+
return /^(["']).*\1$/.test(trimmed) ? trimmed.slice(1, -1) : trimmed;
|
|
18844
|
+
}
|
|
18845
|
+
function parseRule(text3) {
|
|
18846
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text3);
|
|
18847
|
+
if (match === null) {
|
|
18848
|
+
return { paths: [], body: text3 };
|
|
18849
|
+
}
|
|
18850
|
+
const paths = [];
|
|
18851
|
+
const lines2 = (match[1] ?? "").split(/\r?\n/);
|
|
18852
|
+
for (let index = 0; index < lines2.length; index++) {
|
|
18853
|
+
const head = /^paths:\s*(.*)$/.exec(lines2[index] ?? "");
|
|
18854
|
+
if (head === null) {
|
|
18855
|
+
continue;
|
|
18856
|
+
}
|
|
18857
|
+
const inline = (head[1] ?? "").trim();
|
|
18858
|
+
if (inline.startsWith("[")) {
|
|
18859
|
+
paths.push(
|
|
18860
|
+
...inline.replace(/^\[|\]$/g, "").split(",").map(unquote).filter((entry) => entry !== "")
|
|
18861
|
+
);
|
|
18862
|
+
} else if (inline !== "") {
|
|
18863
|
+
paths.push(unquote(inline));
|
|
18864
|
+
} else {
|
|
18865
|
+
for (const next of lines2.slice(index + 1)) {
|
|
18866
|
+
const item = /^\s*-\s+(.*)$/.exec(next);
|
|
18867
|
+
if (item === null) {
|
|
18868
|
+
break;
|
|
18869
|
+
}
|
|
18870
|
+
paths.push(unquote(item[1] ?? ""));
|
|
18871
|
+
}
|
|
18872
|
+
}
|
|
18873
|
+
break;
|
|
18874
|
+
}
|
|
18875
|
+
return { paths, body: text3.slice(match[0].length) };
|
|
18876
|
+
}
|
|
18877
|
+
function agentsRulesSection(rules) {
|
|
18878
|
+
if (rules.length === 0) {
|
|
18879
|
+
return "";
|
|
18880
|
+
}
|
|
18881
|
+
const head = "## Project rules";
|
|
18882
|
+
const parts = [];
|
|
18883
|
+
let used = head.length;
|
|
18884
|
+
let included = 0;
|
|
18885
|
+
for (const rule of rules) {
|
|
18886
|
+
const { paths, body } = parseRule(DECODER2.decode(rule.bytes));
|
|
18887
|
+
const scope = paths.length === 0 ? "" : `
|
|
18888
|
+
_Applies when working on: ${paths.join(", ")}_
|
|
18889
|
+
`;
|
|
18890
|
+
const block = `### Rule: ${rule.path}${scope}
|
|
18891
|
+
${body.trim()}`;
|
|
18892
|
+
if (used + block.length + 2 > AGENTS_RULES_LIMIT) {
|
|
18893
|
+
break;
|
|
18894
|
+
}
|
|
18895
|
+
parts.push(block);
|
|
18896
|
+
used += block.length + 2;
|
|
18897
|
+
included++;
|
|
18898
|
+
}
|
|
18899
|
+
const omitted = rules.length - included;
|
|
18900
|
+
if (omitted > 0) {
|
|
18901
|
+
parts.push(
|
|
18902
|
+
`[Fabric: ${omitted} more rule ${omitted === 1 ? "file was" : "files were"} left out to keep this file short: ${rules.slice(included).map((rule) => rule.path).join(
|
|
18903
|
+
", "
|
|
18904
|
+
)}. Load fabric-instruction-access to find their original source paths and read mapped local copies or available text through the project's Fabric MCP tools.]`
|
|
18905
|
+
);
|
|
18906
|
+
}
|
|
18907
|
+
return `${head}
|
|
18908
|
+
|
|
18909
|
+
${parts.join("\n\n")}`;
|
|
18910
|
+
}
|
|
18911
|
+
function agentsInstructions(memoryText, rules) {
|
|
18912
|
+
const sections = [
|
|
18913
|
+
memoryText === null ? "" : memoryText.trimEnd(),
|
|
18914
|
+
agentsRulesSection(rules)
|
|
18915
|
+
].filter((section) => section !== "");
|
|
18916
|
+
return sections.length === 0 ? null : sections.join("\n\n");
|
|
18917
|
+
}
|
|
18918
|
+
|
|
18919
|
+
// src/lib/plugin/rules-sync.ts
|
|
18920
|
+
var MARKER_NAME = ".fabric-managed.json";
|
|
18921
|
+
var MARKER_VERSION2 = 1;
|
|
18922
|
+
var MAX_MARKER_BYTES2 = 256 * 1024;
|
|
18923
|
+
var MAX_RULE_READ_BYTES = 2 * 1024 * 1024;
|
|
18924
|
+
function rulesFolderFor(projectId) {
|
|
18925
|
+
return `.claude/rules/${pluginNameFor(projectId)}`;
|
|
18926
|
+
}
|
|
18927
|
+
var sha256 = (bytes) => createHash15("sha256").update(bytes).digest("hex");
|
|
18928
|
+
async function readMarker2(root, folder) {
|
|
18929
|
+
try {
|
|
18930
|
+
const read = await readFileSafely(root, `${folder}/${MARKER_NAME}`, {
|
|
18931
|
+
maxBytes: MAX_MARKER_BYTES2
|
|
18932
|
+
});
|
|
18933
|
+
if (read === null) {
|
|
18934
|
+
return {};
|
|
18935
|
+
}
|
|
18936
|
+
const parsed = JSON.parse(
|
|
18937
|
+
new TextDecoder().decode(read.bytes)
|
|
18938
|
+
);
|
|
18939
|
+
const files = parsed?.files;
|
|
18940
|
+
if (parsed?.version !== MARKER_VERSION2 || typeof files !== "object" || files === null || Array.isArray(files)) {
|
|
18941
|
+
return {};
|
|
18942
|
+
}
|
|
18943
|
+
const managed = {};
|
|
18944
|
+
for (const [file, hash] of Object.entries(files)) {
|
|
18945
|
+
if (typeof hash === "string" && /^[0-9a-f]{64}$/.test(hash) && checkRelativePath(file).ok) {
|
|
18946
|
+
managed[file] = hash;
|
|
18947
|
+
}
|
|
18948
|
+
}
|
|
18949
|
+
return managed;
|
|
18950
|
+
} catch {
|
|
18951
|
+
return {};
|
|
18952
|
+
}
|
|
18953
|
+
}
|
|
18954
|
+
async function writeMarker(root, folder, managed) {
|
|
18955
|
+
const sorted = Object.fromEntries(
|
|
18956
|
+
Object.entries(managed).sort(
|
|
18957
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
18958
|
+
)
|
|
18959
|
+
);
|
|
18960
|
+
await writeFileSafely({
|
|
18961
|
+
root,
|
|
18962
|
+
relativePath: `${folder}/${MARKER_NAME}`,
|
|
18963
|
+
bytes: new TextEncoder().encode(
|
|
18964
|
+
`${JSON.stringify({ version: MARKER_VERSION2, files: sorted }, null, 2)}
|
|
18965
|
+
`
|
|
18966
|
+
),
|
|
18967
|
+
mode: 420
|
|
18968
|
+
});
|
|
18969
|
+
}
|
|
18970
|
+
async function removeMarker(root, folder) {
|
|
18971
|
+
const relative = `${folder}/${MARKER_NAME}`;
|
|
18972
|
+
const current = await readFileSafely(root, relative, {
|
|
18973
|
+
maxBytes: MAX_MARKER_BYTES2
|
|
18974
|
+
});
|
|
18975
|
+
if (current !== null) {
|
|
18976
|
+
await deleteFileSafely(root, relative, sha256(current.bytes));
|
|
18977
|
+
}
|
|
18978
|
+
await pruneEmpty(root, folder, MARKER_NAME);
|
|
18979
|
+
}
|
|
18980
|
+
async function pruneEmpty(root, folder, file) {
|
|
18981
|
+
const parts = file.split("/").slice(0, -1);
|
|
18982
|
+
for (let depth = parts.length; depth >= 0; depth--) {
|
|
18983
|
+
const relative = [folder, ...parts.slice(0, depth)].join("/");
|
|
18984
|
+
try {
|
|
18985
|
+
await rmdir2(path33.join(root, relative));
|
|
18986
|
+
} catch {
|
|
18987
|
+
return;
|
|
18988
|
+
}
|
|
18989
|
+
}
|
|
18990
|
+
}
|
|
18991
|
+
async function syncProjectRules(input) {
|
|
18992
|
+
const folder = rulesFolderFor(input.projectId);
|
|
18993
|
+
const { kept: wanted, omitted } = capRules(input.rules);
|
|
18994
|
+
const result = {
|
|
18995
|
+
written: [],
|
|
18996
|
+
removed: [],
|
|
18997
|
+
unchanged: 0,
|
|
18998
|
+
kept: [],
|
|
18999
|
+
skipped: [],
|
|
19000
|
+
omitted,
|
|
19001
|
+
timedOut: false,
|
|
19002
|
+
refused: null
|
|
19003
|
+
};
|
|
19004
|
+
const folderCheck = await assertWritableTarget(
|
|
19005
|
+
input.root,
|
|
19006
|
+
`${folder}/${MARKER_NAME}`
|
|
19007
|
+
);
|
|
19008
|
+
if (!folderCheck.ok) {
|
|
19009
|
+
result.refused = `${folder} cannot be used (${folderCheck.reason}: ${folderCheck.detail})`;
|
|
19010
|
+
return result;
|
|
19011
|
+
}
|
|
19012
|
+
const previous = await readMarker2(input.root, folder);
|
|
19013
|
+
const next = {};
|
|
19014
|
+
const expired = () => input.deadline !== void 0 && Date.now() > input.deadline;
|
|
19015
|
+
const handled = /* @__PURE__ */ new Set();
|
|
19016
|
+
for (const rule of wanted) {
|
|
19017
|
+
const relative = rule.path;
|
|
19018
|
+
if (!checkRelativePath(relative).ok || relative === MARKER_NAME) {
|
|
19019
|
+
result.skipped.push({ path: relative, reason: "unsafe-path" });
|
|
19020
|
+
continue;
|
|
19021
|
+
}
|
|
19022
|
+
handled.add(relative);
|
|
19023
|
+
if (input.tracked?.has(relative)) {
|
|
19024
|
+
result.skipped.push({ path: relative, reason: "tracked" });
|
|
19025
|
+
continue;
|
|
19026
|
+
}
|
|
19027
|
+
if (expired()) {
|
|
19028
|
+
result.timedOut = true;
|
|
19029
|
+
if (previous[relative] !== void 0) {
|
|
19030
|
+
next[relative] = previous[relative];
|
|
19031
|
+
}
|
|
19032
|
+
continue;
|
|
19033
|
+
}
|
|
19034
|
+
const target = `${folder}/${relative}`;
|
|
19035
|
+
const wantedHash = sha256(rule.bytes);
|
|
19036
|
+
try {
|
|
19037
|
+
const check = await assertWritableTarget(input.root, target);
|
|
19038
|
+
if (!check.ok) {
|
|
19039
|
+
result.kept.push({ path: relative, reason: "unsafe" });
|
|
19040
|
+
continue;
|
|
19041
|
+
}
|
|
19042
|
+
const existing = await readFileSafely(input.root, target, {
|
|
19043
|
+
maxBytes: MAX_RULE_READ_BYTES
|
|
19044
|
+
}).catch(() => "unreadable");
|
|
19045
|
+
if (existing === "unreadable") {
|
|
19046
|
+
result.kept.push({ path: relative, reason: "unsafe" });
|
|
19047
|
+
continue;
|
|
19048
|
+
}
|
|
19049
|
+
if (existing === null) {
|
|
19050
|
+
await writeFileSafely({
|
|
19051
|
+
root: input.root,
|
|
19052
|
+
relativePath: target,
|
|
19053
|
+
bytes: rule.bytes,
|
|
19054
|
+
mode: 420
|
|
19055
|
+
});
|
|
19056
|
+
result.written.push(relative);
|
|
19057
|
+
next[relative] = wantedHash;
|
|
19058
|
+
continue;
|
|
19059
|
+
}
|
|
19060
|
+
const currentHash = sha256(existing.bytes);
|
|
19061
|
+
if (currentHash === wantedHash) {
|
|
19062
|
+
result.unchanged++;
|
|
19063
|
+
next[relative] = wantedHash;
|
|
19064
|
+
} else if (previous[relative] === currentHash) {
|
|
19065
|
+
await writeFileSafely({
|
|
19066
|
+
root: input.root,
|
|
19067
|
+
relativePath: target,
|
|
19068
|
+
bytes: rule.bytes,
|
|
19069
|
+
mode: 420
|
|
19070
|
+
});
|
|
19071
|
+
result.written.push(relative);
|
|
19072
|
+
next[relative] = wantedHash;
|
|
19073
|
+
} else {
|
|
19074
|
+
result.kept.push({
|
|
19075
|
+
path: relative,
|
|
19076
|
+
reason: previous[relative] === void 0 ? "not-managed" : "edited"
|
|
19077
|
+
});
|
|
19078
|
+
}
|
|
19079
|
+
} catch {
|
|
19080
|
+
result.kept.push({ path: relative, reason: "unsafe" });
|
|
19081
|
+
}
|
|
19082
|
+
}
|
|
19083
|
+
for (const [relative, hash] of Object.entries(previous)) {
|
|
19084
|
+
if (handled.has(relative)) {
|
|
19085
|
+
continue;
|
|
19086
|
+
}
|
|
19087
|
+
if (expired()) {
|
|
19088
|
+
result.timedOut = true;
|
|
19089
|
+
next[relative] = hash;
|
|
19090
|
+
continue;
|
|
19091
|
+
}
|
|
19092
|
+
try {
|
|
19093
|
+
const outcome = await deleteFileSafely(
|
|
19094
|
+
input.root,
|
|
19095
|
+
`${folder}/${relative}`,
|
|
19096
|
+
hash
|
|
19097
|
+
);
|
|
19098
|
+
if (outcome === "modified") {
|
|
19099
|
+
result.kept.push({ path: relative, reason: "edited" });
|
|
19100
|
+
} else {
|
|
19101
|
+
if (outcome === "deleted") {
|
|
19102
|
+
result.removed.push(relative);
|
|
19103
|
+
}
|
|
19104
|
+
await pruneEmpty(input.root, folder, relative);
|
|
19105
|
+
}
|
|
19106
|
+
} catch {
|
|
19107
|
+
result.kept.push({ path: relative, reason: "unsafe" });
|
|
19108
|
+
}
|
|
19109
|
+
}
|
|
19110
|
+
const changed = JSON.stringify(Object.entries(next).sort()) !== JSON.stringify(Object.entries(previous).sort());
|
|
19111
|
+
try {
|
|
19112
|
+
if (Object.keys(next).length > 0) {
|
|
19113
|
+
if (changed) {
|
|
19114
|
+
await writeMarker(input.root, folder, next);
|
|
19115
|
+
}
|
|
19116
|
+
} else if (Object.keys(previous).length > 0) {
|
|
19117
|
+
await removeMarker(input.root, folder);
|
|
19118
|
+
}
|
|
19119
|
+
} catch {
|
|
19120
|
+
result.refused = `${folder}/${MARKER_NAME} could not be updated`;
|
|
19121
|
+
}
|
|
19122
|
+
return result;
|
|
19123
|
+
}
|
|
19124
|
+
function rulesSyncLines(result) {
|
|
19125
|
+
const lines2 = [];
|
|
19126
|
+
if (result.refused !== null) {
|
|
19127
|
+
lines2.push(`rules not synced: ${result.refused}.`);
|
|
19128
|
+
}
|
|
19129
|
+
if (result.written.length > 0 || result.removed.length > 0) {
|
|
19130
|
+
lines2.push(
|
|
19131
|
+
`rules: ${result.written.length} written, ${result.removed.length} removed.`
|
|
19132
|
+
);
|
|
19133
|
+
}
|
|
19134
|
+
for (const entry of result.kept) {
|
|
19135
|
+
lines2.push(
|
|
19136
|
+
entry.reason === "edited" ? `rules: kept ${entry.path}, which you edited; it is no longer updated from Fabric.` : entry.reason === "not-managed" ? `rules: kept ${entry.path}, which is your own file at that path.` : `rules: left ${entry.path} alone because it could not be written safely.`
|
|
19137
|
+
);
|
|
19138
|
+
}
|
|
19139
|
+
for (const entry of result.skipped) {
|
|
19140
|
+
lines2.push(
|
|
19141
|
+
entry.reason === "tracked" ? `rules: skipped ${entry.path}, which this project's own git tracks.` : `rules: skipped ${entry.path}: not a path Fabric will write.`
|
|
19142
|
+
);
|
|
19143
|
+
}
|
|
19144
|
+
if (result.omitted > 0) {
|
|
19145
|
+
lines2.push(
|
|
19146
|
+
`rules: ${result.omitted} more rule files were not delivered automatically (limit: 64 files or 256 KiB). Load fabric-instruction-access to find their original paths and read mapped local copies or available text through the project's Fabric MCP tools.`
|
|
19147
|
+
);
|
|
19148
|
+
}
|
|
19149
|
+
if (result.timedOut) {
|
|
19150
|
+
lines2.push(
|
|
19151
|
+
"rules: ran out of time; the rest follow at the next session."
|
|
19152
|
+
);
|
|
19153
|
+
}
|
|
19154
|
+
return lines2;
|
|
19155
|
+
}
|
|
19156
|
+
|
|
19157
|
+
// src/commands/instructions/render.ts
|
|
19158
|
+
import os2 from "os";
|
|
19159
|
+
import path36 from "path";
|
|
18108
19160
|
|
|
18109
19161
|
// src/lib/plugin/render.ts
|
|
18110
|
-
import { createHash as
|
|
19162
|
+
import { createHash as createHash18, randomBytes as randomBytes4 } from "crypto";
|
|
18111
19163
|
import {
|
|
18112
19164
|
chmod as chmod2,
|
|
18113
19165
|
mkdir as mkdir13,
|
|
18114
19166
|
readdir as readdir3,
|
|
18115
|
-
readFile as
|
|
19167
|
+
readFile as readFile11,
|
|
18116
19168
|
rename as rename7,
|
|
18117
19169
|
rm as rm7,
|
|
18118
19170
|
stat as stat8,
|
|
18119
19171
|
writeFile as writeFile9
|
|
18120
19172
|
} from "fs/promises";
|
|
18121
|
-
import
|
|
19173
|
+
import path35 from "path";
|
|
18122
19174
|
|
|
18123
19175
|
// src/lib/plugin/source.ts
|
|
18124
|
-
import { createHash as
|
|
18125
|
-
import { mkdir as mkdir12, readFile as
|
|
18126
|
-
import
|
|
19176
|
+
import { createHash as createHash17 } from "crypto";
|
|
19177
|
+
import { mkdir as mkdir12, readFile as readFile10, rename as rename6, rm as rm6, writeFile as writeFile8 } from "fs/promises";
|
|
19178
|
+
import path34 from "path";
|
|
18127
19179
|
|
|
18128
19180
|
// src/lib/git-blob.ts
|
|
18129
|
-
import { createHash as
|
|
19181
|
+
import { createHash as createHash16 } from "crypto";
|
|
18130
19182
|
function objectFormatOf(oid) {
|
|
18131
19183
|
if (/^[0-9a-f]{40}$/.test(oid)) {
|
|
18132
19184
|
return "sha1";
|
|
@@ -18137,7 +19189,7 @@ function objectFormatOf(oid) {
|
|
|
18137
19189
|
return null;
|
|
18138
19190
|
}
|
|
18139
19191
|
function gitBlobOid(bytes, format = "sha1") {
|
|
18140
|
-
return
|
|
19192
|
+
return createHash16(format).update(`blob ${bytes.byteLength}\0`).update(bytes).digest("hex");
|
|
18141
19193
|
}
|
|
18142
19194
|
function matchesBlobOid(bytes, oid) {
|
|
18143
19195
|
const format = objectFormatOf(oid);
|
|
@@ -18145,18 +19197,18 @@ function matchesBlobOid(bytes, oid) {
|
|
|
18145
19197
|
}
|
|
18146
19198
|
|
|
18147
19199
|
// src/lib/plugin/source.ts
|
|
18148
|
-
var
|
|
18149
|
-
var
|
|
19200
|
+
var READ_BATCH_SIZE = 200;
|
|
19201
|
+
var MAX_INLINE_BYTES = 256 * 1024;
|
|
18150
19202
|
var ALWAYS_ON = /* @__PURE__ */ new Set(["CLAUDE.md", "AGENTS.md"]);
|
|
18151
19203
|
function isWantedPath(instructionPath) {
|
|
18152
|
-
return ALWAYS_ON.has(instructionPath) || pluginPathFor(instructionPath) !== null;
|
|
19204
|
+
return ALWAYS_ON.has(instructionPath) || pluginPathFor(instructionPath) !== null || projectRulePathFor(instructionPath) !== null;
|
|
18153
19205
|
}
|
|
18154
19206
|
function blobPath(cacheDirectory, key) {
|
|
18155
|
-
return
|
|
19207
|
+
return path34.join(cacheDirectory, "blobs", key);
|
|
18156
19208
|
}
|
|
18157
19209
|
async function cached(cacheDirectory, key, valid) {
|
|
18158
19210
|
try {
|
|
18159
|
-
const bytes = await
|
|
19211
|
+
const bytes = await readFile10(blobPath(cacheDirectory, key));
|
|
18160
19212
|
return valid(bytes) ? bytes : null;
|
|
18161
19213
|
} catch {
|
|
18162
19214
|
return null;
|
|
@@ -18166,7 +19218,7 @@ async function remember(cacheDirectory, key, bytes) {
|
|
|
18166
19218
|
const file = blobPath(cacheDirectory, key);
|
|
18167
19219
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
18168
19220
|
try {
|
|
18169
|
-
await mkdir12(
|
|
19221
|
+
await mkdir12(path34.dirname(file), { recursive: true });
|
|
18170
19222
|
await writeFile8(temporary, bytes);
|
|
18171
19223
|
await rename6(temporary, file);
|
|
18172
19224
|
} catch {
|
|
@@ -18174,38 +19226,13 @@ async function remember(cacheDirectory, key, bytes) {
|
|
|
18174
19226
|
}
|
|
18175
19227
|
}
|
|
18176
19228
|
function sha256Of2(bytes) {
|
|
18177
|
-
return
|
|
18178
|
-
}
|
|
18179
|
-
async function inBatches(items, work) {
|
|
18180
|
-
const queue = items.values();
|
|
18181
|
-
const failures = [];
|
|
18182
|
-
const worker = async () => {
|
|
18183
|
-
for (; ; ) {
|
|
18184
|
-
const next = queue.next();
|
|
18185
|
-
if (next.done || failures.length > 0) {
|
|
18186
|
-
return;
|
|
18187
|
-
}
|
|
18188
|
-
try {
|
|
18189
|
-
await work(next.value);
|
|
18190
|
-
} catch (error) {
|
|
18191
|
-
failures.push(error);
|
|
18192
|
-
}
|
|
18193
|
-
}
|
|
18194
|
-
};
|
|
18195
|
-
await Promise.all(
|
|
18196
|
-
Array.from(
|
|
18197
|
-
{ length: Math.min(READ_CONCURRENCY, items.length) },
|
|
18198
|
-
worker
|
|
18199
|
-
)
|
|
18200
|
-
);
|
|
18201
|
-
if (failures.length > 0) {
|
|
18202
|
-
throw failures[0];
|
|
18203
|
-
}
|
|
19229
|
+
return createHash17("sha256").update(bytes).digest("hex");
|
|
18204
19230
|
}
|
|
18205
19231
|
async function readDirect(input) {
|
|
18206
19232
|
const listing = await input.client.instructions.listRepositoryFiles(
|
|
18207
19233
|
input.projectId,
|
|
18208
|
-
{ org: input.org }
|
|
19234
|
+
{ org: input.org },
|
|
19235
|
+
{ signal: input.signal }
|
|
18209
19236
|
);
|
|
18210
19237
|
if (listing.incomplete || listing.refusal !== null) {
|
|
18211
19238
|
throw new Error(
|
|
@@ -18216,59 +19243,104 @@ async function readDirect(input) {
|
|
|
18216
19243
|
generation: listing.generation,
|
|
18217
19244
|
commitSha: listing.commitSha
|
|
18218
19245
|
};
|
|
19246
|
+
const listed = listing.files.map((file) => ({
|
|
19247
|
+
path: file.path,
|
|
19248
|
+
size: file.size,
|
|
19249
|
+
kind: file.kind,
|
|
19250
|
+
blobId: file.blobId,
|
|
19251
|
+
mode: file.mode
|
|
19252
|
+
}));
|
|
18219
19253
|
const wanted = listing.files.filter((file) => isWantedPath(file.path));
|
|
18220
19254
|
const files = [];
|
|
18221
19255
|
const skipped3 = [];
|
|
18222
|
-
const
|
|
19256
|
+
const missing = [];
|
|
19257
|
+
if (listing.files.some((file) => !checkRelativePath(file.path).ok) || findCollision(listing.files.map((file) => file.path)) !== null) {
|
|
19258
|
+
throw new Error("The repository returned unsafe or colliding paths.");
|
|
19259
|
+
}
|
|
19260
|
+
for (const file of wanted) {
|
|
19261
|
+
input.signal?.throwIfAborted();
|
|
18223
19262
|
const blobId = file.blobId;
|
|
18224
|
-
if (blobId
|
|
18225
|
-
|
|
18226
|
-
|
|
18227
|
-
`git-${blobId}`,
|
|
18228
|
-
(b) => matchesBlobOid(b, blobId)
|
|
19263
|
+
if (blobId === void 0 || objectFormatOf(blobId) === null) {
|
|
19264
|
+
throw new Error(
|
|
19265
|
+
`The repository did not identify the Git blob for ${file.path}.`
|
|
18229
19266
|
);
|
|
18230
|
-
if (hit !== null) {
|
|
18231
|
-
files.push({
|
|
18232
|
-
path: file.path,
|
|
18233
|
-
bytes: hit,
|
|
18234
|
-
mode: modeOf(file.mode)
|
|
18235
|
-
});
|
|
18236
|
-
return;
|
|
18237
|
-
}
|
|
18238
19267
|
}
|
|
18239
|
-
const
|
|
18240
|
-
|
|
18241
|
-
|
|
18242
|
-
|
|
19268
|
+
const hit = await cached(
|
|
19269
|
+
input.cacheDirectory,
|
|
19270
|
+
`git-${blobId}`,
|
|
19271
|
+
(b) => b.byteLength <= MAX_INLINE_BYTES && matchesBlobOid(b, blobId)
|
|
19272
|
+
);
|
|
19273
|
+
if (hit !== null) {
|
|
19274
|
+
files.push({
|
|
19275
|
+
path: file.path,
|
|
19276
|
+
bytes: hit,
|
|
19277
|
+
mode: modeOf(file.mode)
|
|
19278
|
+
});
|
|
19279
|
+
continue;
|
|
19280
|
+
}
|
|
19281
|
+
missing.push(file);
|
|
19282
|
+
}
|
|
19283
|
+
for (let offset = 0; offset < missing.length; offset += READ_BATCH_SIZE) {
|
|
19284
|
+
const batch = missing.slice(offset, offset + READ_BATCH_SIZE);
|
|
19285
|
+
const paths = batch.map((file) => file.path);
|
|
19286
|
+
let startIndex = 0;
|
|
19287
|
+
for (; ; ) {
|
|
19288
|
+
input.signal?.throwIfAborted();
|
|
19289
|
+
const page = await input.client.instructions.readRepositoryFiles(
|
|
18243
19290
|
input.projectId,
|
|
18244
|
-
{ ...pin,
|
|
19291
|
+
{ ...pin, paths, startIndex },
|
|
19292
|
+
{ org: input.org, signal: input.signal }
|
|
18245
19293
|
);
|
|
18246
|
-
|
|
18247
|
-
|
|
18248
|
-
|
|
19294
|
+
input.signal?.throwIfAborted();
|
|
19295
|
+
const end = page.nextIndex === null ? paths.length : page.nextIndex;
|
|
19296
|
+
if (page.generation !== pin.generation || page.commitSha !== pin.commitSha || !Number.isInteger(end) || end <= startIndex || end > paths.length || !Array.isArray(page.files) || page.files.length !== end - startIndex) {
|
|
19297
|
+
throw new Error(
|
|
19298
|
+
"The repository returned an invalid pinned batch."
|
|
19299
|
+
);
|
|
18249
19300
|
}
|
|
18250
|
-
|
|
18251
|
-
|
|
18252
|
-
|
|
18253
|
-
|
|
18254
|
-
|
|
18255
|
-
|
|
18256
|
-
|
|
18257
|
-
|
|
19301
|
+
for (const [index, result] of page.files.entries()) {
|
|
19302
|
+
const file = batch[startIndex + index];
|
|
19303
|
+
if (file === void 0 || result.path !== file.path) {
|
|
19304
|
+
throw new Error(
|
|
19305
|
+
"The repository returned unexpected or missing paths."
|
|
19306
|
+
);
|
|
19307
|
+
}
|
|
19308
|
+
const read = result.read;
|
|
19309
|
+
if (read.state === "binary" || read.state === "tooLarge") {
|
|
19310
|
+
skipped3.push(file.path);
|
|
19311
|
+
continue;
|
|
19312
|
+
}
|
|
19313
|
+
if (read.state !== "found" || typeof read.text !== "string") {
|
|
19314
|
+
throw new Error(
|
|
19315
|
+
`The repository could not read ${file.path}.`
|
|
19316
|
+
);
|
|
19317
|
+
}
|
|
19318
|
+
const bytes = new TextEncoder().encode(read.text);
|
|
19319
|
+
if (bytes.byteLength !== read.size || bytes.byteLength > MAX_INLINE_BYTES || file.blobId === void 0 || !matchesBlobOid(bytes, file.blobId)) {
|
|
19320
|
+
throw new Error(
|
|
19321
|
+
`The repository returned different bytes for ${file.path}.`
|
|
18258
19322
|
);
|
|
18259
19323
|
}
|
|
19324
|
+
await remember(
|
|
19325
|
+
input.cacheDirectory,
|
|
19326
|
+
`git-${file.blobId}`,
|
|
19327
|
+
bytes
|
|
19328
|
+
);
|
|
18260
19329
|
files.push({ path: file.path, bytes, mode: modeOf(file.mode) });
|
|
18261
|
-
return;
|
|
18262
19330
|
}
|
|
18263
|
-
|
|
19331
|
+
if (page.nextIndex === null) {
|
|
19332
|
+
break;
|
|
19333
|
+
}
|
|
19334
|
+
startIndex = end;
|
|
18264
19335
|
}
|
|
18265
|
-
|
|
18266
|
-
|
|
18267
|
-
await inBatches(wanted, readOne);
|
|
19336
|
+
}
|
|
19337
|
+
input.signal?.throwIfAborted();
|
|
18268
19338
|
return {
|
|
18269
19339
|
pin: `repo:${pin.generation}:${pin.commitSha}`,
|
|
19340
|
+
source: { kind: "repository", ...pin },
|
|
18270
19341
|
files,
|
|
18271
|
-
skipped: skipped3.sort()
|
|
19342
|
+
skipped: skipped3.sort(),
|
|
19343
|
+
listed
|
|
18272
19344
|
};
|
|
18273
19345
|
}
|
|
18274
19346
|
function modeOf(mode) {
|
|
@@ -18281,9 +19353,22 @@ function modeOf(mode) {
|
|
|
18281
19353
|
async function readUploaded(input) {
|
|
18282
19354
|
const { published } = input;
|
|
18283
19355
|
if (!published.published || published.snapshot === void 0) {
|
|
18284
|
-
return {
|
|
19356
|
+
return {
|
|
19357
|
+
pin: "upload:none",
|
|
19358
|
+
source: { kind: "upload", digest: null },
|
|
19359
|
+
files: [],
|
|
19360
|
+
skipped: [],
|
|
19361
|
+
listed: []
|
|
19362
|
+
};
|
|
18285
19363
|
}
|
|
18286
19364
|
const digest = published.snapshot.digest;
|
|
19365
|
+
const listed = (published.manifest ?? []).map((entry) => ({
|
|
19366
|
+
path: entry.path,
|
|
19367
|
+
size: entry.size,
|
|
19368
|
+
kind: entry.kind,
|
|
19369
|
+
sha256: entry.sha256,
|
|
19370
|
+
mode: entry.mode
|
|
19371
|
+
}));
|
|
18287
19372
|
const manifest = (published.manifest ?? []).filter(
|
|
18288
19373
|
(entry) => isWantedPath(entry.path)
|
|
18289
19374
|
);
|
|
@@ -18302,7 +19387,7 @@ async function readUploaded(input) {
|
|
|
18302
19387
|
}
|
|
18303
19388
|
}
|
|
18304
19389
|
if (missing.length > 0) {
|
|
18305
|
-
const
|
|
19390
|
+
const byPath5 = new Map(missing.map((entry) => [entry.path, entry]));
|
|
18306
19391
|
await downloadFilesByUrl(
|
|
18307
19392
|
{
|
|
18308
19393
|
client: input.client,
|
|
@@ -18323,12 +19408,18 @@ async function readUploaded(input) {
|
|
|
18323
19408
|
files.push({
|
|
18324
19409
|
path: file.path,
|
|
18325
19410
|
bytes,
|
|
18326
|
-
mode:
|
|
19411
|
+
mode: byPath5.get(file.path)?.mode ?? null
|
|
18327
19412
|
});
|
|
18328
19413
|
}
|
|
18329
19414
|
);
|
|
18330
19415
|
}
|
|
18331
|
-
return {
|
|
19416
|
+
return {
|
|
19417
|
+
pin: `upload:${digest}`,
|
|
19418
|
+
source: { kind: "upload", digest },
|
|
19419
|
+
files,
|
|
19420
|
+
skipped: [],
|
|
19421
|
+
listed
|
|
19422
|
+
};
|
|
18332
19423
|
}
|
|
18333
19424
|
async function readInstructions(input) {
|
|
18334
19425
|
const direct = input.published.direct;
|
|
@@ -18346,6 +19437,16 @@ async function readInstructions(input) {
|
|
|
18346
19437
|
// src/lib/plugin/render.ts
|
|
18347
19438
|
var KEPT_PINS = 3;
|
|
18348
19439
|
var CODEX_PIN_MARKER = ".fabric-render";
|
|
19440
|
+
var LEFT_OUT_NAMED = 3;
|
|
19441
|
+
function leftOutLine(paths) {
|
|
19442
|
+
if (paths.length === 0) {
|
|
19443
|
+
return null;
|
|
19444
|
+
}
|
|
19445
|
+
const named = paths.slice(0, LEFT_OUT_NAMED).join(", ");
|
|
19446
|
+
const more = paths.length > LEFT_OUT_NAMED ? ` and ${paths.length - LEFT_OUT_NAMED} more` : "";
|
|
19447
|
+
const count = paths.length === 1 ? "1 instruction file is" : `${paths.length} instruction files are`;
|
|
19448
|
+
return `${count} not part of the plugin (${named}${more}). Their paths remain in the fabric-instruction-access skill's catalog; use the project's Fabric MCP tools to read available text on demand. The Coding Instructions tab lists the layout reasons.`;
|
|
19449
|
+
}
|
|
18349
19450
|
function memoryFoldersOf(targets) {
|
|
18350
19451
|
return {
|
|
18351
19452
|
state: "known",
|
|
@@ -18369,7 +19470,7 @@ async function readMemoryFolders(input) {
|
|
|
18369
19470
|
}
|
|
18370
19471
|
}
|
|
18371
19472
|
function pinOf(sourcePin, memory, tool) {
|
|
18372
|
-
return
|
|
19473
|
+
return createHash18("sha256").update(
|
|
18373
19474
|
JSON.stringify({
|
|
18374
19475
|
v: RENDER_VERSION,
|
|
18375
19476
|
tool,
|
|
@@ -18386,8 +19487,8 @@ async function exists(file) {
|
|
|
18386
19487
|
}
|
|
18387
19488
|
async function writeTree(directory, files) {
|
|
18388
19489
|
for (const file of files) {
|
|
18389
|
-
const target =
|
|
18390
|
-
await mkdir13(
|
|
19490
|
+
const target = path35.join(directory, ...file.path.split("/"));
|
|
19491
|
+
await mkdir13(path35.dirname(target), { recursive: true });
|
|
18391
19492
|
await writeFile9(target, file.bytes);
|
|
18392
19493
|
await chmod2(target, file.mode).catch(() => void 0);
|
|
18393
19494
|
}
|
|
@@ -18403,12 +19504,12 @@ async function prune(projectDirectory, keep) {
|
|
|
18403
19504
|
const dated = await Promise.all(
|
|
18404
19505
|
entries.filter((entry) => entry.isDirectory() && entry.name !== keep).map(async (entry) => ({
|
|
18405
19506
|
name: entry.name,
|
|
18406
|
-
modified: (await stat8(
|
|
19507
|
+
modified: (await stat8(path35.join(projectDirectory, entry.name))).mtimeMs
|
|
18407
19508
|
}))
|
|
18408
19509
|
);
|
|
18409
19510
|
dated.sort((a, b) => b.modified - a.modified);
|
|
18410
19511
|
for (const old of dated.slice(KEPT_PINS - 1)) {
|
|
18411
|
-
await rm7(
|
|
19512
|
+
await rm7(path35.join(projectDirectory, old.name), {
|
|
18412
19513
|
recursive: true,
|
|
18413
19514
|
force: true
|
|
18414
19515
|
}).catch(() => void 0);
|
|
@@ -18417,8 +19518,8 @@ async function prune(projectDirectory, keep) {
|
|
|
18417
19518
|
}
|
|
18418
19519
|
}
|
|
18419
19520
|
async function installClaude(projectDirectory, pin, files) {
|
|
18420
|
-
const directory =
|
|
18421
|
-
if (await exists(
|
|
19521
|
+
const directory = path35.join(projectDirectory, pin);
|
|
19522
|
+
if (await exists(path35.join(directory, ".claude-plugin", "plugin.json"))) {
|
|
18422
19523
|
return { directory, reused: true };
|
|
18423
19524
|
}
|
|
18424
19525
|
await mkdir13(projectDirectory, { recursive: true });
|
|
@@ -18431,7 +19532,7 @@ async function installClaude(projectDirectory, pin, files) {
|
|
|
18431
19532
|
await rm7(temporary, { recursive: true, force: true }).catch(
|
|
18432
19533
|
() => void 0
|
|
18433
19534
|
);
|
|
18434
|
-
if (await exists(
|
|
19535
|
+
if (await exists(path35.join(directory, ".claude-plugin", "plugin.json"))) {
|
|
18435
19536
|
return { directory, reused: true };
|
|
18436
19537
|
}
|
|
18437
19538
|
throw error;
|
|
@@ -18439,20 +19540,30 @@ async function installClaude(projectDirectory, pin, files) {
|
|
|
18439
19540
|
await prune(projectDirectory, pin);
|
|
18440
19541
|
return { directory, reused: false };
|
|
18441
19542
|
}
|
|
18442
|
-
async function installCodex(directory, pin, files) {
|
|
18443
|
-
|
|
18444
|
-
|
|
19543
|
+
async function installCodex(directory, pin, files, binding) {
|
|
19544
|
+
await assertLocationFree({
|
|
19545
|
+
directory,
|
|
19546
|
+
location: "codex-plugin",
|
|
19547
|
+
wanted: binding
|
|
19548
|
+
});
|
|
19549
|
+
const marker = await readFile11(
|
|
19550
|
+
path35.join(directory, CODEX_PIN_MARKER),
|
|
18445
19551
|
"utf8"
|
|
18446
19552
|
).catch(() => null);
|
|
18447
19553
|
if (marker?.trim() === pin) {
|
|
18448
19554
|
return { directory, reused: true };
|
|
18449
19555
|
}
|
|
18450
|
-
await mkdir13(
|
|
19556
|
+
await mkdir13(path35.dirname(directory), { recursive: true });
|
|
18451
19557
|
const temporary = temporaryNameFor(directory);
|
|
18452
19558
|
const retired = temporaryNameFor(directory).replace(".tmp-", ".old-");
|
|
18453
19559
|
try {
|
|
18454
19560
|
await writeTree(temporary, [
|
|
18455
19561
|
...files,
|
|
19562
|
+
{
|
|
19563
|
+
path: BINDING_FILE,
|
|
19564
|
+
bytes: new TextEncoder().encode(bindingText(binding)),
|
|
19565
|
+
mode: 420
|
|
19566
|
+
},
|
|
18456
19567
|
{
|
|
18457
19568
|
path: CODEX_PIN_MARKER,
|
|
18458
19569
|
bytes: new TextEncoder().encode(`${pin}
|
|
@@ -18493,7 +19604,15 @@ async function renderPlugin(input) {
|
|
|
18493
19604
|
fetchImpl: input.fetchImpl
|
|
18494
19605
|
});
|
|
18495
19606
|
for (const skipped3 of read.skipped) {
|
|
18496
|
-
input.log?.(
|
|
19607
|
+
input.log?.(
|
|
19608
|
+
`${skipped3} is unavailable as inline text, so the plugin leaves its bytes out. Its path remains in the fabric-instruction-access catalog; MCP reads may report a binary or size refusal.`
|
|
19609
|
+
);
|
|
19610
|
+
}
|
|
19611
|
+
const notPartOfPlugin = leftOutLine(
|
|
19612
|
+
planPluginFiles(read.listed, input.tool).leftOut.filter((entry) => entry.reason !== "always-on-shadowed").map((entry) => entry.path)
|
|
19613
|
+
);
|
|
19614
|
+
if (notPartOfPlugin !== null) {
|
|
19615
|
+
input.log?.(notPartOfPlugin);
|
|
18497
19616
|
}
|
|
18498
19617
|
const memory = await readMemoryFolders(input);
|
|
18499
19618
|
const pin = pinOf(read.pin, memory, input.tool);
|
|
@@ -18503,25 +19622,39 @@ async function renderPlugin(input) {
|
|
|
18503
19622
|
files: read.files,
|
|
18504
19623
|
memory,
|
|
18505
19624
|
gatewayUrl: projectResource(input.origin, "mcp", input.projectId),
|
|
18506
|
-
pin
|
|
19625
|
+
pin,
|
|
19626
|
+
instructionAccess: { listed: read.listed, source: read.source }
|
|
18507
19627
|
});
|
|
18508
19628
|
const always = alwaysOnFile(read.files, input.tool);
|
|
19629
|
+
const guidance = instructionAccessBootstrap(
|
|
19630
|
+
pluginNameFor(input.projectId),
|
|
19631
|
+
input.tool
|
|
19632
|
+
);
|
|
18509
19633
|
const installed = input.tool === "claude-code" ? await installClaude(
|
|
18510
|
-
|
|
19634
|
+
path35.join(input.cacheDirectory, "plugins", input.projectId),
|
|
18511
19635
|
pin,
|
|
18512
19636
|
files
|
|
18513
19637
|
) : await installCodex(
|
|
18514
|
-
|
|
19638
|
+
path35.join(
|
|
18515
19639
|
input.codexPluginsDirectory,
|
|
18516
19640
|
pluginNameFor(input.projectId)
|
|
18517
19641
|
),
|
|
18518
19642
|
pin,
|
|
18519
|
-
files
|
|
19643
|
+
files,
|
|
19644
|
+
{
|
|
19645
|
+
projectId: input.projectId,
|
|
19646
|
+
origin: originOf4(input.origin)
|
|
19647
|
+
}
|
|
18520
19648
|
);
|
|
18521
19649
|
return {
|
|
18522
19650
|
...installed,
|
|
18523
19651
|
pin,
|
|
18524
|
-
alwaysOn:
|
|
19652
|
+
alwaysOn: {
|
|
19653
|
+
file: always?.file ?? "Fabric instruction access",
|
|
19654
|
+
text: `${guidance}${always === null ? "" : `
|
|
19655
|
+
${always.text}`}`
|
|
19656
|
+
},
|
|
19657
|
+
rules: selectRules(read, input.tool),
|
|
18525
19658
|
files
|
|
18526
19659
|
};
|
|
18527
19660
|
}
|
|
@@ -18537,7 +19670,7 @@ function toolOf(target) {
|
|
|
18537
19670
|
}
|
|
18538
19671
|
throw new CliFailure("--target is claude-code or codex", 2);
|
|
18539
19672
|
}
|
|
18540
|
-
|
|
19673
|
+
function connectForProject(input) {
|
|
18541
19674
|
const origin = normalizeOrigin(
|
|
18542
19675
|
input.baseUrl ?? getBaseUrl() ?? DEFAULT_ORIGIN
|
|
18543
19676
|
);
|
|
@@ -18553,20 +19686,26 @@ async function renderForProject(input) {
|
|
|
18553
19686
|
3
|
|
18554
19687
|
);
|
|
18555
19688
|
}
|
|
18556
|
-
|
|
18557
|
-
|
|
18558
|
-
|
|
18559
|
-
|
|
18560
|
-
|
|
18561
|
-
|
|
19689
|
+
return {
|
|
19690
|
+
origin,
|
|
19691
|
+
client: getClient({
|
|
19692
|
+
baseUrl: input.baseUrl,
|
|
19693
|
+
project: input.project,
|
|
19694
|
+
timeoutMs: REQUEST_TIMEOUT_MS
|
|
19695
|
+
}).withoutContext(),
|
|
19696
|
+
cacheDirectory: path36.join(path36.dirname(getConfigPath()), "cache")
|
|
19697
|
+
};
|
|
19698
|
+
}
|
|
19699
|
+
async function renderForProject(input) {
|
|
19700
|
+
const { origin, client, cacheDirectory } = connectForProject(input);
|
|
18562
19701
|
const rendered = await renderPlugin({
|
|
18563
19702
|
client,
|
|
18564
19703
|
projectId: input.project,
|
|
18565
19704
|
org: input.org,
|
|
18566
19705
|
origin,
|
|
18567
19706
|
tool: input.tool,
|
|
18568
|
-
cacheDirectory: input.out !== void 0 && input.tool === "claude-code" ?
|
|
18569
|
-
codexPluginsDirectory: input.out !== void 0 && input.tool === "codex" ?
|
|
19707
|
+
cacheDirectory: input.out !== void 0 && input.tool === "claude-code" ? path36.resolve(input.out) : cacheDirectory,
|
|
19708
|
+
codexPluginsDirectory: input.out !== void 0 && input.tool === "codex" ? path36.resolve(input.out) : path36.join(
|
|
18570
19709
|
machine.home() ?? os2.homedir(),
|
|
18571
19710
|
".codex",
|
|
18572
19711
|
"plugins"
|
|
@@ -18574,7 +19713,16 @@ async function renderForProject(input) {
|
|
|
18574
19713
|
timeoutMs: REQUEST_TIMEOUT_MS,
|
|
18575
19714
|
log: input.log
|
|
18576
19715
|
});
|
|
18577
|
-
return {
|
|
19716
|
+
return {
|
|
19717
|
+
directory: rendered.directory,
|
|
19718
|
+
reused: rendered.reused,
|
|
19719
|
+
pin: rendered.pin,
|
|
19720
|
+
alwaysOn: rendered.alwaysOn,
|
|
19721
|
+
agentsText: input.tool === "codex" ? agentsInstructions(
|
|
19722
|
+
rendered.alwaysOn?.text ?? null,
|
|
19723
|
+
rendered.rules
|
|
19724
|
+
) : null
|
|
19725
|
+
};
|
|
18578
19726
|
}
|
|
18579
19727
|
async function refreshAgentsOverride(root, instructions) {
|
|
18580
19728
|
const canonical = await resolveExistingRoot(root);
|
|
@@ -18621,12 +19769,26 @@ function registerRenderCommand(instructions) {
|
|
|
18621
19769
|
log: (text3) => process.stderr.write(`fabric: ${text3}
|
|
18622
19770
|
`)
|
|
18623
19771
|
});
|
|
18624
|
-
if (tool === "codex" && rendered.
|
|
19772
|
+
if (tool === "codex" && rendered.agentsText !== null) {
|
|
18625
19773
|
await refreshAgentsOverride(
|
|
18626
19774
|
opts.dest ?? process.cwd(),
|
|
18627
|
-
rendered.
|
|
19775
|
+
rendered.agentsText
|
|
18628
19776
|
);
|
|
18629
19777
|
}
|
|
19778
|
+
if (tool === "codex" && opts.out === void 0) {
|
|
19779
|
+
const env2 = machine.env();
|
|
19780
|
+
const home = machine.home() ?? os2.homedir();
|
|
19781
|
+
await refreshCodexInstall({
|
|
19782
|
+
run: createAgentRunner({
|
|
19783
|
+
lookup: { env: env2, platform: machine.platform() }
|
|
19784
|
+
}),
|
|
19785
|
+
home,
|
|
19786
|
+
codexHome: env2.CODEX_HOME ?? path36.join(home, ".codex"),
|
|
19787
|
+
projectId: opts.project,
|
|
19788
|
+
version: codexPluginVersion(rendered.pin),
|
|
19789
|
+
cwd: path36.resolve(opts.dest ?? process.cwd())
|
|
19790
|
+
});
|
|
19791
|
+
}
|
|
18630
19792
|
if (!opts.hook) {
|
|
18631
19793
|
process.stdout.write(`${rendered.directory}
|
|
18632
19794
|
`);
|
|
@@ -18645,6 +19807,111 @@ function registerRenderCommand(instructions) {
|
|
|
18645
19807
|
});
|
|
18646
19808
|
}
|
|
18647
19809
|
|
|
19810
|
+
// src/commands/instructions/rules.ts
|
|
19811
|
+
import path37 from "path";
|
|
19812
|
+
var HOOK_BUDGET_MS = 4e4;
|
|
19813
|
+
var REQUEST_TIMEOUT_MS2 = 2e4;
|
|
19814
|
+
async function trackedBelow(toplevel, root, folder, deadline) {
|
|
19815
|
+
const prefix = path37.relative(toplevel, path37.join(root, folder)).split(path37.sep).join("/");
|
|
19816
|
+
const tracked = await trackedPaths(toplevel, [prefix], deadline);
|
|
19817
|
+
return new Set(
|
|
19818
|
+
tracked.kind === "ok" ? tracked.value.filter((entry) => entry.startsWith(`${prefix}/`)).map((entry) => entry.slice(prefix.length + 1)) : []
|
|
19819
|
+
);
|
|
19820
|
+
}
|
|
19821
|
+
async function syncRulesForProject(input) {
|
|
19822
|
+
const budget = input.budgetMs ?? HOOK_BUDGET_MS;
|
|
19823
|
+
const deadline = Date.now() + budget;
|
|
19824
|
+
const { client, cacheDirectory } = connectForProject(input);
|
|
19825
|
+
const published = await client.instructions.getPublished(input.project, {
|
|
19826
|
+
org: input.org
|
|
19827
|
+
});
|
|
19828
|
+
const read = await readInstructions({
|
|
19829
|
+
client,
|
|
19830
|
+
projectId: input.project,
|
|
19831
|
+
org: input.org,
|
|
19832
|
+
published,
|
|
19833
|
+
cacheDirectory,
|
|
19834
|
+
timeoutMs: REQUEST_TIMEOUT_MS2,
|
|
19835
|
+
signal: AbortSignal.timeout(budget)
|
|
19836
|
+
});
|
|
19837
|
+
const rules = selectRules(read, "claude-code");
|
|
19838
|
+
const root = await resolveExistingRoot(input.root);
|
|
19839
|
+
const folder = rulesFolderFor(input.project);
|
|
19840
|
+
const tree = await findWorkTree(root, Date.now() + 8e3);
|
|
19841
|
+
const toplevel = tree.kind === "ok" ? tree.value.toplevel : null;
|
|
19842
|
+
const result = await syncProjectRules({
|
|
19843
|
+
root,
|
|
19844
|
+
projectId: input.project,
|
|
19845
|
+
rules,
|
|
19846
|
+
tracked: toplevel === null ? void 0 : await trackedBelow(
|
|
19847
|
+
toplevel,
|
|
19848
|
+
root,
|
|
19849
|
+
folder,
|
|
19850
|
+
Date.now() + 8e3
|
|
19851
|
+
),
|
|
19852
|
+
deadline
|
|
19853
|
+
});
|
|
19854
|
+
if (toplevel !== null && rules.length > 0 && result.refused === null) {
|
|
19855
|
+
await excludeLocalFiles({
|
|
19856
|
+
toplevel,
|
|
19857
|
+
directory: root,
|
|
19858
|
+
files: [folder],
|
|
19859
|
+
deadline: Date.now() + 8e3
|
|
19860
|
+
});
|
|
19861
|
+
}
|
|
19862
|
+
return result;
|
|
19863
|
+
}
|
|
19864
|
+
function registerRulesCommand(instructions) {
|
|
19865
|
+
instructions.command("rules").description(
|
|
19866
|
+
"Write the project's rules into .claude/rules/fabric-<id>/ for Claude Code (a plugin cannot carry rules)"
|
|
19867
|
+
).requiredOption("--project <id>", "Project ID").option(
|
|
19868
|
+
"--base-url <origin>",
|
|
19869
|
+
"Deployment to talk to (default: the one you are signed in to)"
|
|
19870
|
+
).option("--org <slug>", "Organization context").option(
|
|
19871
|
+
"--dest <dir>",
|
|
19872
|
+
"The project folder (default: the folder `init` set up, found from cwd)"
|
|
19873
|
+
).option("--hook", "Run as a session hook: print nothing, never fail").action(async (opts) => {
|
|
19874
|
+
const note = (text3) => process.stderr.write(`fabric: ${text3}
|
|
19875
|
+
`);
|
|
19876
|
+
try {
|
|
19877
|
+
const project = opts.project;
|
|
19878
|
+
let root;
|
|
19879
|
+
if (opts.dest !== void 0) {
|
|
19880
|
+
root = await resolveDestinationRoot(
|
|
19881
|
+
path37.resolve(opts.dest)
|
|
19882
|
+
);
|
|
19883
|
+
} else {
|
|
19884
|
+
const found = await findProjectRoot(process.cwd());
|
|
19885
|
+
if (found === null || found.marker.projectId !== project) {
|
|
19886
|
+
throw new CliFailure(
|
|
19887
|
+
"this folder was not set up by `fabric instructions init` for this project; pass --dest",
|
|
19888
|
+
2
|
|
19889
|
+
);
|
|
19890
|
+
}
|
|
19891
|
+
root = found.root;
|
|
19892
|
+
}
|
|
19893
|
+
const result = await syncRulesForProject({
|
|
19894
|
+
project,
|
|
19895
|
+
baseUrl: opts.baseUrl,
|
|
19896
|
+
org: opts.org,
|
|
19897
|
+
root
|
|
19898
|
+
});
|
|
19899
|
+
for (const text3 of rulesSyncLines(result)) {
|
|
19900
|
+
note(text3);
|
|
19901
|
+
}
|
|
19902
|
+
} catch (error) {
|
|
19903
|
+
note(
|
|
19904
|
+
`rules failed: ${error instanceof CliFailure ? error.message : describeError(error)}`
|
|
19905
|
+
);
|
|
19906
|
+
if (!opts.hook) {
|
|
19907
|
+
process.exit(
|
|
19908
|
+
error instanceof CliFailure ? error.exitCode : 1
|
|
19909
|
+
);
|
|
19910
|
+
}
|
|
19911
|
+
}
|
|
19912
|
+
});
|
|
19913
|
+
}
|
|
19914
|
+
|
|
18648
19915
|
// src/commands/instructions/init-plugin.ts
|
|
18649
19916
|
function wordForPaste(value) {
|
|
18650
19917
|
return pathAsCommandWord(value) ?? value;
|
|
@@ -18673,6 +19940,25 @@ async function approveClaudePlugin(input) {
|
|
|
18673
19940
|
);
|
|
18674
19941
|
return installed.kind === "exited" && installed.code === 0 ? { kind: "approved" } : { kind: "manual", lines: [commands.install] };
|
|
18675
19942
|
}
|
|
19943
|
+
async function deliverRules(options, root) {
|
|
19944
|
+
try {
|
|
19945
|
+
const result = await syncRulesForProject({
|
|
19946
|
+
project: options.project,
|
|
19947
|
+
baseUrl: options.origin,
|
|
19948
|
+
org: options.org,
|
|
19949
|
+
root
|
|
19950
|
+
});
|
|
19951
|
+
const notes = rulesSyncLines(result);
|
|
19952
|
+
return result.written.length + result.unchanged > 0 || notes.length > 0 ? [
|
|
19953
|
+
`Project rules are delivered to ${rulesFolderFor(options.project)}/ (kept out of commits; refreshed at each session start).`,
|
|
19954
|
+
...notes
|
|
19955
|
+
] : [];
|
|
19956
|
+
} catch (error) {
|
|
19957
|
+
return [
|
|
19958
|
+
`The project's rules could not be delivered now (${describeError(error)}); the session hook retries at each start.`
|
|
19959
|
+
];
|
|
19960
|
+
}
|
|
19961
|
+
}
|
|
18676
19962
|
function line(text3) {
|
|
18677
19963
|
process.stdout.write(`${text3}
|
|
18678
19964
|
`);
|
|
@@ -18701,7 +19987,7 @@ async function probeMemory(options, root) {
|
|
|
18701
19987
|
}
|
|
18702
19988
|
async function runPluginInit(options) {
|
|
18703
19989
|
const explicit = options.tool === void 0 ? void 0 : toolOf2(options.tool);
|
|
18704
|
-
const destination =
|
|
19990
|
+
const destination = path38.resolve(options.dest ?? process.cwd());
|
|
18705
19991
|
const root = await resolveDestinationRoot(destination);
|
|
18706
19992
|
try {
|
|
18707
19993
|
await assertKeyStaysOutside(root, getConfigPath());
|
|
@@ -18719,7 +20005,7 @@ async function runPluginInit(options) {
|
|
|
18719
20005
|
const tools = explicit === void 0 ? detected.tools : [explicit];
|
|
18720
20006
|
const launcher = await resolveHookLauncher({
|
|
18721
20007
|
origin: options.origin,
|
|
18722
|
-
configDirectory:
|
|
20008
|
+
configDirectory: path38.dirname(getConfigPath()),
|
|
18723
20009
|
lookup: { env: machine.env(), platform: machine.platform() },
|
|
18724
20010
|
bundle: isServedBundle() ? { scriptPath: bundleScriptPath() } : null
|
|
18725
20011
|
});
|
|
@@ -18747,11 +20033,12 @@ async function runPluginInit(options) {
|
|
|
18747
20033
|
org: options.org
|
|
18748
20034
|
});
|
|
18749
20035
|
marketplaceDirectory = await writeClaudeMarketplace({
|
|
18750
|
-
cacheDirectory:
|
|
18751
|
-
|
|
20036
|
+
cacheDirectory: path38.join(
|
|
20037
|
+
path38.dirname(getConfigPath()),
|
|
18752
20038
|
"cache"
|
|
18753
20039
|
),
|
|
18754
20040
|
projectId: options.project,
|
|
20041
|
+
origin: options.origin,
|
|
18755
20042
|
command
|
|
18756
20043
|
});
|
|
18757
20044
|
const registered = await registerClaudePlugin({
|
|
@@ -18760,8 +20047,9 @@ async function runPluginInit(options) {
|
|
|
18760
20047
|
marketplaceDirectory
|
|
18761
20048
|
});
|
|
18762
20049
|
excludes.push(CLAUDE_SETTINGS_RELATIVE_PATH);
|
|
20050
|
+
steps.push(...await deliverRules(options, root));
|
|
18763
20051
|
steps.push(
|
|
18764
|
-
`Registered the ${pluginNameFor(options.project)} plugin in ${
|
|
20052
|
+
`Registered the ${pluginNameFor(options.project)} plugin in ${path38.relative(root, registered.settingsPath) || registered.settingsPath} (marketplace folder ${marketplaceDirectory}); Claude Code builds it from Fabric at the start of each session.`
|
|
18765
20053
|
);
|
|
18766
20054
|
} else {
|
|
18767
20055
|
const registration = await registerCodexPlugin({
|
|
@@ -18770,13 +20058,22 @@ async function runPluginInit(options) {
|
|
|
18770
20058
|
pluginDirectory: rendered.directory
|
|
18771
20059
|
});
|
|
18772
20060
|
steps.push(
|
|
18773
|
-
`Rendered the ${pluginNameFor(options.project)} plugin to ${rendered.directory} and listed it in ${registration.marketplaceFile} (marketplace "${registration.marketplaceName}")
|
|
20061
|
+
`Rendered the ${pluginNameFor(options.project)} plugin to ${rendered.directory} and listed it in ${registration.marketplaceFile} (marketplace "${registration.marketplaceName}").`
|
|
18774
20062
|
);
|
|
18775
|
-
|
|
18776
|
-
|
|
20063
|
+
const installed = await installCodexPlugin({
|
|
20064
|
+
run: run4,
|
|
20065
|
+
projectId: options.project,
|
|
20066
|
+
marketplaceName: registration.marketplaceName,
|
|
20067
|
+
cwd: root
|
|
20068
|
+
});
|
|
20069
|
+
steps.push(
|
|
20070
|
+
installed.kind === "installed" ? `Installed the ${pluginNameFor(options.project)} plugin in Codex.` : `Codex did not install the plugin. Run: ${installed.line ?? "codex plugin add, with the plugin and marketplace names above"}`
|
|
20071
|
+
);
|
|
20072
|
+
if (rendered.agentsText !== null) {
|
|
20073
|
+
await refreshAgentsOverride(root, rendered.agentsText);
|
|
18777
20074
|
excludes.push("AGENTS.override.md");
|
|
18778
20075
|
steps.push(
|
|
18779
|
-
"Wrote AGENTS.override.md (kept out of commits): the project's AGENTS.md, then the instructions."
|
|
20076
|
+
"Wrote AGENTS.override.md (kept out of commits): the project's AGENTS.md, then the instructions and the project's rules."
|
|
18780
20077
|
);
|
|
18781
20078
|
}
|
|
18782
20079
|
}
|
|
@@ -18958,6 +20255,7 @@ function buildInstructionsCommand() {
|
|
|
18958
20255
|
"Keep a checkout current with a project's published coding instructions"
|
|
18959
20256
|
);
|
|
18960
20257
|
registerRenderCommand(instructions);
|
|
20258
|
+
registerRulesCommand(instructions);
|
|
18961
20259
|
instructions.command("self-update", { hidden: true }).description("Refresh the kept copy of the served CLI, out of band").option("--base-url <origin>", BASE_URL_HELP).action(async (opts) => {
|
|
18962
20260
|
await refreshKeptCopyAfterHook(
|
|
18963
20261
|
opts,
|
|
@@ -19207,7 +20505,7 @@ async function traceHookFailure(opts, why) {
|
|
|
19207
20505
|
return;
|
|
19208
20506
|
}
|
|
19209
20507
|
const reason = why.signedOut ? "not-signed-in" : why.upgrade ? "upgrade-required" : why.timedOut ? "deadline" : `failed-${why.exitCode}`;
|
|
19210
|
-
await appendTrace(traceFile(
|
|
20508
|
+
await appendTrace(traceFile(path39.dirname(getConfigPath())), {
|
|
19211
20509
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19212
20510
|
projectId: opts.project ?? "",
|
|
19213
20511
|
outcome: "skipped",
|
|
@@ -19240,7 +20538,7 @@ async function refreshKeptCopyAfterHook(opts, deadlineAt, { mayDefer = true } =
|
|
|
19240
20538
|
const input = {
|
|
19241
20539
|
origin,
|
|
19242
20540
|
script: bundleScriptPath(),
|
|
19243
|
-
configDirectory:
|
|
20541
|
+
configDirectory: path39.dirname(getConfigPath()),
|
|
19244
20542
|
runningTarball: bundleTarballPath(),
|
|
19245
20543
|
env: process.env,
|
|
19246
20544
|
deadlineAt,
|
|
@@ -19258,7 +20556,7 @@ async function refreshKeptCopyAfterHook(opts, deadlineAt, { mayDefer = true } =
|
|
|
19258
20556
|
});
|
|
19259
20557
|
}
|
|
19260
20558
|
if (outcome.kind !== "skipped") {
|
|
19261
|
-
await appendTrace(traceFile(
|
|
20559
|
+
await appendTrace(traceFile(path39.dirname(getConfigPath())), {
|
|
19262
20560
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19263
20561
|
projectId: "",
|
|
19264
20562
|
outcome: "self-update",
|
|
@@ -19318,7 +20616,7 @@ function gitDeadline() {
|
|
|
19318
20616
|
return activeDeadlineAt === void 0 ? Date.now() + GIT_BUDGET_MS3 : activeDeadlineAt - hookTiming.gitMarginMs;
|
|
19319
20617
|
}
|
|
19320
20618
|
function destinationOf(opts) {
|
|
19321
|
-
return
|
|
20619
|
+
return path39.resolve(opts.dest ?? process.cwd());
|
|
19322
20620
|
}
|
|
19323
20621
|
function instructionsClient(opts, timeoutMs, { neverRetry = false } = {}) {
|
|
19324
20622
|
if (!getApiKey(deploymentOrigin(opts), opts.project)) {
|
|
@@ -19691,7 +20989,7 @@ async function directRepositoryCheckoutStatus(input) {
|
|
|
19691
20989
|
};
|
|
19692
20990
|
}
|
|
19693
20991
|
async function readLockForProject2(destination, projectId) {
|
|
19694
|
-
const lock = await
|
|
20992
|
+
const lock = await readLock2(destination);
|
|
19695
20993
|
if (lock !== null && lock.projectId !== projectId) {
|
|
19696
20994
|
throw outcomeFailure("lock-of-other-project", {});
|
|
19697
20995
|
}
|
|
@@ -20104,7 +21402,7 @@ async function syncOnce(opts) {
|
|
|
20104
21402
|
projectId: opts.project,
|
|
20105
21403
|
deadline: gitDeadline(),
|
|
20106
21404
|
optedOut: opts.fastForward === false,
|
|
20107
|
-
traceFile: traceFile(
|
|
21405
|
+
traceFile: traceFile(path39.dirname(getConfigPath()))
|
|
20108
21406
|
}) : null;
|
|
20109
21407
|
hookTraced = fastForward !== null;
|
|
20110
21408
|
lazyTransport?.rethrow();
|
|
@@ -20145,7 +21443,7 @@ async function syncOnce(opts) {
|
|
|
20145
21443
|
projectId: opts.project,
|
|
20146
21444
|
deadline: gitDeadline(),
|
|
20147
21445
|
optedOut: opts.fastForward === false,
|
|
20148
|
-
traceFile: traceFile(
|
|
21446
|
+
traceFile: traceFile(path39.dirname(getConfigPath())),
|
|
20149
21447
|
timings: hookPhases
|
|
20150
21448
|
}) : null;
|
|
20151
21449
|
return {
|
|
@@ -20223,7 +21521,7 @@ async function syncOnce(opts) {
|
|
|
20223
21521
|
try {
|
|
20224
21522
|
for (let attempt2 = 0; plan.writes.length > 0 && attempt2 < 2; attempt2++) {
|
|
20225
21523
|
try {
|
|
20226
|
-
const
|
|
21524
|
+
const byPath5 = new Map(
|
|
20227
21525
|
manifest.map((entry) => [entry.path, entry])
|
|
20228
21526
|
);
|
|
20229
21527
|
const large = manifest.reduce((total, entry) => total + entry.size, 0) > IN_MEMORY_SNAPSHOT_BYTES;
|
|
@@ -20235,7 +21533,7 @@ async function syncOnce(opts) {
|
|
|
20235
21533
|
digest: published.snapshot.digest,
|
|
20236
21534
|
files: plan.writes.map((entry) => ({
|
|
20237
21535
|
path: entry.path,
|
|
20238
|
-
size:
|
|
21536
|
+
size: byPath5.get(entry.path)?.size ?? -1,
|
|
20239
21537
|
sha256: entry.sha256 ?? ""
|
|
20240
21538
|
})),
|
|
20241
21539
|
timeoutMs: opts.hook ? hookDeadlineMs() : BUNDLE_TIMEOUT_MS,
|
|
@@ -20260,7 +21558,7 @@ async function syncOnce(opts) {
|
|
|
20260
21558
|
archive,
|
|
20261
21559
|
plan.writes.map((entry) => ({
|
|
20262
21560
|
path: entry.path,
|
|
20263
|
-
size:
|
|
21561
|
+
size: byPath5.get(entry.path)?.size ?? -1
|
|
20264
21562
|
}))
|
|
20265
21563
|
);
|
|
20266
21564
|
}
|
|
@@ -20803,7 +22101,7 @@ async function cloneProjectRepository(opts, destination, repository, transport)
|
|
|
20803
22101
|
}
|
|
20804
22102
|
async function insideInstructionsCheckout(opts) {
|
|
20805
22103
|
const destination = await resolveExistingRoot(destinationOf(opts));
|
|
20806
|
-
if (await
|
|
22104
|
+
if (await readLock2(destination) !== null) {
|
|
20807
22105
|
return true;
|
|
20808
22106
|
}
|
|
20809
22107
|
const guard = new SourceGuard(destination, opts.remote);
|
|
@@ -20880,7 +22178,7 @@ async function runInit(opts, format) {
|
|
|
20880
22178
|
);
|
|
20881
22179
|
cloned = true;
|
|
20882
22180
|
if (cloneFolder !== void 0 && checkout.class === "unmapped") {
|
|
20883
|
-
destination =
|
|
22181
|
+
destination = path39.join(checkout.toplevel, checkout.rootPath);
|
|
20884
22182
|
existing = await resolveExistingRoot(destination);
|
|
20885
22183
|
checkout = await classifyCheckout({
|
|
20886
22184
|
destination: existing,
|
|
@@ -20948,12 +22246,12 @@ async function runInit(opts, format) {
|
|
|
20948
22246
|
throw outcomeFailure("lessons-need-claude", {});
|
|
20949
22247
|
}
|
|
20950
22248
|
if (matching !== null) {
|
|
20951
|
-
const prefix =
|
|
22249
|
+
const prefix = path39.relative(matching.toplevel, root);
|
|
20952
22250
|
const relativeSetupPaths = [
|
|
20953
22251
|
...tools.tools.map(
|
|
20954
|
-
(tool) =>
|
|
22252
|
+
(tool) => path39.join(prefix, hookPathFor(tool)).replaceAll(path39.sep, "/")
|
|
20955
22253
|
),
|
|
20956
|
-
|
|
22254
|
+
path39.join(prefix, LOCK_DIRECTORY, LOCK_FILENAME).replaceAll(path39.sep, "/")
|
|
20957
22255
|
];
|
|
20958
22256
|
const tracked = await trackedPaths(
|
|
20959
22257
|
matching.toplevel,
|
|
@@ -20992,7 +22290,7 @@ async function runInit(opts, format) {
|
|
|
20992
22290
|
const applies = opts.reportOnly ? false : matching !== null ? true : Boolean(opts.apply);
|
|
20993
22291
|
const launcher = await resolveHookLauncher({
|
|
20994
22292
|
origin: deploymentOrigin(opts),
|
|
20995
|
-
configDirectory:
|
|
22293
|
+
configDirectory: path39.dirname(getConfigPath()),
|
|
20996
22294
|
lookup: { env: machine.env(), platform: machine.platform() },
|
|
20997
22295
|
bundle: isServedBundle() ? { scriptPath: bundleScriptPath() } : null
|
|
20998
22296
|
});
|
|
@@ -21164,7 +22462,7 @@ async function runInit(opts, format) {
|
|
|
21164
22462
|
outcomeLine("cloned-into", {
|
|
21165
22463
|
where: folderForCommand(
|
|
21166
22464
|
opts.dest,
|
|
21167
|
-
|
|
22465
|
+
path39.relative(
|
|
21168
22466
|
await resolveExistingRoot(destinationOf(opts)),
|
|
21169
22467
|
root
|
|
21170
22468
|
)
|
|
@@ -21210,8 +22508,8 @@ async function runLessonPromptCommand(opts) {
|
|
|
21210
22508
|
stdinText = await readAllStdin(process.stdin);
|
|
21211
22509
|
} catch {
|
|
21212
22510
|
}
|
|
21213
|
-
const markerDir =
|
|
21214
|
-
|
|
22511
|
+
const markerDir = path39.join(
|
|
22512
|
+
path39.dirname(getConfigPath()),
|
|
21215
22513
|
"lesson-prompts"
|
|
21216
22514
|
);
|
|
21217
22515
|
const result = await runLessonPrompt({
|
|
@@ -21373,6 +22671,9 @@ function buildMcpCommand() {
|
|
|
21373
22671
|
// src/commands/memory/index.ts
|
|
21374
22672
|
import { Command as Command15 } from "commander";
|
|
21375
22673
|
|
|
22674
|
+
// src/lib/memory/pull.ts
|
|
22675
|
+
import path41 from "path";
|
|
22676
|
+
|
|
21376
22677
|
// src/lib/memory/context.ts
|
|
21377
22678
|
var FETCH_CONCURRENCY = 4;
|
|
21378
22679
|
function now(context) {
|
|
@@ -21403,11 +22704,11 @@ async function inParallel(items, work) {
|
|
|
21403
22704
|
}
|
|
21404
22705
|
|
|
21405
22706
|
// src/lib/memory/local.ts
|
|
21406
|
-
import { createHash as
|
|
21407
|
-
import { lstat as
|
|
21408
|
-
import
|
|
22707
|
+
import { createHash as createHash19 } from "crypto";
|
|
22708
|
+
import { lstat as lstat7, readdir as readdir5 } from "fs/promises";
|
|
22709
|
+
import path40 from "path";
|
|
21409
22710
|
function sha256Hex2(bytes) {
|
|
21410
|
-
return
|
|
22711
|
+
return createHash19("sha256").update(bytes).digest("hex");
|
|
21411
22712
|
}
|
|
21412
22713
|
async function scanFolder(root, folder) {
|
|
21413
22714
|
const scan = { files: [], skipped: [] };
|
|
@@ -21416,7 +22717,7 @@ async function scanFolder(root, folder) {
|
|
|
21416
22717
|
const directory = pending.pop();
|
|
21417
22718
|
let names2;
|
|
21418
22719
|
try {
|
|
21419
|
-
names2 = await readdir5(
|
|
22720
|
+
names2 = await readdir5(path40.join(root, ...directory.split("/")));
|
|
21420
22721
|
} catch (error) {
|
|
21421
22722
|
if (error.code === "ENOENT") {
|
|
21422
22723
|
continue;
|
|
@@ -21428,8 +22729,8 @@ async function scanFolder(root, folder) {
|
|
|
21428
22729
|
if (name.startsWith(".")) {
|
|
21429
22730
|
continue;
|
|
21430
22731
|
}
|
|
21431
|
-
const stats = await
|
|
21432
|
-
|
|
22732
|
+
const stats = await lstat7(
|
|
22733
|
+
path40.join(root, ...relative.split("/"))
|
|
21433
22734
|
).catch(() => null);
|
|
21434
22735
|
if (stats === null) {
|
|
21435
22736
|
continue;
|
|
@@ -21491,7 +22792,7 @@ function emptyMemoryLock(projectId) {
|
|
|
21491
22792
|
outbox: []
|
|
21492
22793
|
};
|
|
21493
22794
|
}
|
|
21494
|
-
function
|
|
22795
|
+
function isRecord8(value) {
|
|
21495
22796
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21496
22797
|
}
|
|
21497
22798
|
function damaged() {
|
|
@@ -21512,7 +22813,7 @@ async function readMemoryLock(root, projectId) {
|
|
|
21512
22813
|
} catch {
|
|
21513
22814
|
throw damaged();
|
|
21514
22815
|
}
|
|
21515
|
-
if (!
|
|
22816
|
+
if (!isRecord8(parsed) || parsed.version !== MEMORY_LOCK_VERSION || typeof parsed.projectId !== "string" || !isRecord8(parsed.files) || !Array.isArray(parsed.outbox)) {
|
|
21516
22817
|
throw damaged();
|
|
21517
22818
|
}
|
|
21518
22819
|
if (parsed.projectId !== projectId) {
|
|
@@ -21713,14 +23014,14 @@ function planPull(input) {
|
|
|
21713
23014
|
const remotePaths = /* @__PURE__ */ new Set();
|
|
21714
23015
|
for (const remote of input.remote) {
|
|
21715
23016
|
remotePaths.add(remote.path);
|
|
21716
|
-
const
|
|
21717
|
-
if (input.tracked.has(
|
|
21718
|
-
actions.push({ kind: "tracked", path:
|
|
23017
|
+
const path42 = remote.path;
|
|
23018
|
+
if (input.tracked.has(path42)) {
|
|
23019
|
+
actions.push({ kind: "tracked", path: path42 });
|
|
21719
23020
|
continue;
|
|
21720
23021
|
}
|
|
21721
|
-
const local = input.local.get(
|
|
21722
|
-
const entry = input.lock.files[
|
|
21723
|
-
const accept = input.acceptRemote.has(
|
|
23022
|
+
const local = input.local.get(path42);
|
|
23023
|
+
const entry = input.lock.files[path42];
|
|
23024
|
+
const accept = input.acceptRemote.has(path42);
|
|
21724
23025
|
if (entry === void 0) {
|
|
21725
23026
|
if (local === void 0) {
|
|
21726
23027
|
actions.push({ kind: "write", file: remote, reason: "new" });
|
|
@@ -21735,7 +23036,7 @@ function planPull(input) {
|
|
|
21735
23036
|
} else {
|
|
21736
23037
|
actions.push({
|
|
21737
23038
|
kind: "keep",
|
|
21738
|
-
path:
|
|
23039
|
+
path: path42,
|
|
21739
23040
|
reason: "untracked-differs"
|
|
21740
23041
|
});
|
|
21741
23042
|
}
|
|
@@ -21748,7 +23049,7 @@ function planPull(input) {
|
|
|
21748
23049
|
actions.push(
|
|
21749
23050
|
accept ? { kind: "write", file: remote, reason: "updated" } : {
|
|
21750
23051
|
kind: "keep",
|
|
21751
|
-
path:
|
|
23052
|
+
path: path42,
|
|
21752
23053
|
reason: "deleted-locally-changed-remotely"
|
|
21753
23054
|
}
|
|
21754
23055
|
);
|
|
@@ -21773,33 +23074,35 @@ function planPull(input) {
|
|
|
21773
23074
|
} else if (accept) {
|
|
21774
23075
|
actions.push({ kind: "write", file: remote, reason: "updated" });
|
|
21775
23076
|
} else {
|
|
21776
|
-
actions.push({ kind: "keep", path:
|
|
23077
|
+
actions.push({ kind: "keep", path: path42, reason: "modified" });
|
|
21777
23078
|
}
|
|
21778
23079
|
}
|
|
21779
|
-
for (const [
|
|
21780
|
-
if (remotePaths.has(
|
|
23080
|
+
for (const [path42, entry] of Object.entries(input.lock.files)) {
|
|
23081
|
+
if (remotePaths.has(path42)) {
|
|
21781
23082
|
continue;
|
|
21782
23083
|
}
|
|
21783
23084
|
if (entry.origin === "push") {
|
|
21784
23085
|
continue;
|
|
21785
23086
|
}
|
|
21786
|
-
const local = input.local.get(
|
|
23087
|
+
const local = input.local.get(path42);
|
|
21787
23088
|
if (local === void 0) {
|
|
21788
|
-
actions.push({ kind: "forget", path:
|
|
21789
|
-
} else if (local === entry.contentHash) {
|
|
21790
|
-
actions.push({
|
|
21791
|
-
kind: "delete",
|
|
21792
|
-
path: path38,
|
|
21793
|
-
expectedHash: entry.contentHash
|
|
21794
|
-
});
|
|
23089
|
+
actions.push({ kind: "forget", path: path42 });
|
|
23090
|
+
} else if (local === entry.contentHash || input.acceptRemote.has(path42)) {
|
|
23091
|
+
actions.push({ kind: "delete", path: path42, expectedHash: local });
|
|
21795
23092
|
} else {
|
|
21796
23093
|
actions.push({
|
|
21797
23094
|
kind: "keep",
|
|
21798
|
-
path:
|
|
23095
|
+
path: path42,
|
|
21799
23096
|
reason: "modified-and-removed"
|
|
21800
23097
|
});
|
|
21801
23098
|
}
|
|
21802
23099
|
}
|
|
23100
|
+
for (const path42 of input.acceptRemote) {
|
|
23101
|
+
const local = input.local.get(path42);
|
|
23102
|
+
if (local !== void 0 && !remotePaths.has(path42) && input.lock.files[path42] === void 0 && !input.tracked.has(path42)) {
|
|
23103
|
+
actions.push({ kind: "delete", path: path42, expectedHash: local });
|
|
23104
|
+
}
|
|
23105
|
+
}
|
|
21803
23106
|
return actions.sort((a, b) => {
|
|
21804
23107
|
const left = actionPath(a);
|
|
21805
23108
|
const right = actionPath(b);
|
|
@@ -21809,13 +23112,13 @@ function planPull(input) {
|
|
|
21809
23112
|
function actionPath(action) {
|
|
21810
23113
|
return action.kind === "write" || action.kind === "adopt" ? action.file.path : action.path;
|
|
21811
23114
|
}
|
|
21812
|
-
function targetOf2(
|
|
21813
|
-
return targets.find((target) =>
|
|
23115
|
+
function targetOf2(path42, targets) {
|
|
23116
|
+
return targets.find((target) => path42.startsWith(`${target.folder}/`));
|
|
21814
23117
|
}
|
|
21815
|
-
function recordFor(outbox,
|
|
23118
|
+
function recordFor(outbox, path42, contentHash) {
|
|
21816
23119
|
return outbox.find(
|
|
21817
23120
|
(record) => record.paths.some(
|
|
21818
|
-
(entry) => entry.path ===
|
|
23121
|
+
(entry) => entry.path === path42 && entry.contentHash === contentHash
|
|
21819
23122
|
)
|
|
21820
23123
|
);
|
|
21821
23124
|
}
|
|
@@ -21841,29 +23144,29 @@ function planPush(input) {
|
|
|
21841
23144
|
}
|
|
21842
23145
|
plan.changes.push(change);
|
|
21843
23146
|
};
|
|
21844
|
-
for (const [
|
|
21845
|
-
const target = targetOf2(
|
|
23147
|
+
for (const [path42, hash] of input.local) {
|
|
23148
|
+
const target = targetOf2(path42, input.targets);
|
|
21846
23149
|
if (target === void 0) {
|
|
21847
23150
|
continue;
|
|
21848
23151
|
}
|
|
21849
|
-
const entry = input.lock.files[
|
|
21850
|
-
if (input.tracked.has(
|
|
21851
|
-
plan.skipped.push({ path:
|
|
23152
|
+
const entry = input.lock.files[path42];
|
|
23153
|
+
if (input.tracked.has(path42)) {
|
|
23154
|
+
plan.skipped.push({ path: path42, reason: "tracked" });
|
|
21852
23155
|
continue;
|
|
21853
23156
|
}
|
|
21854
23157
|
if (entry === void 0) {
|
|
21855
23158
|
consider(
|
|
21856
|
-
{ path:
|
|
23159
|
+
{ path: path42, targetId: target.id, kind: "new", contentHash: hash },
|
|
21857
23160
|
hash
|
|
21858
23161
|
);
|
|
21859
23162
|
} else if (entry.contentHash !== hash) {
|
|
21860
23163
|
if (entry.blobOid === null) {
|
|
21861
|
-
plan.skipped.push({ path:
|
|
23164
|
+
plan.skipped.push({ path: path42, reason: "version-unknown" });
|
|
21862
23165
|
continue;
|
|
21863
23166
|
}
|
|
21864
23167
|
consider(
|
|
21865
23168
|
{
|
|
21866
|
-
path:
|
|
23169
|
+
path: path42,
|
|
21867
23170
|
targetId: target.id,
|
|
21868
23171
|
kind: "modified",
|
|
21869
23172
|
ifMatchOid: entry.blobOid,
|
|
@@ -21873,21 +23176,21 @@ function planPush(input) {
|
|
|
21873
23176
|
);
|
|
21874
23177
|
}
|
|
21875
23178
|
}
|
|
21876
|
-
for (const [
|
|
21877
|
-
if (input.local.has(
|
|
23179
|
+
for (const [path42, entry] of Object.entries(input.lock.files)) {
|
|
23180
|
+
if (input.local.has(path42)) {
|
|
21878
23181
|
continue;
|
|
21879
23182
|
}
|
|
21880
|
-
const target = targetOf2(
|
|
23183
|
+
const target = targetOf2(path42, input.targets);
|
|
21881
23184
|
if (target === void 0) {
|
|
21882
23185
|
continue;
|
|
21883
23186
|
}
|
|
21884
23187
|
if (entry.blobOid === null) {
|
|
21885
|
-
plan.skipped.push({ path:
|
|
23188
|
+
plan.skipped.push({ path: path42, reason: "version-unknown" });
|
|
21886
23189
|
continue;
|
|
21887
23190
|
}
|
|
21888
23191
|
consider(
|
|
21889
23192
|
{
|
|
21890
|
-
path:
|
|
23193
|
+
path: path42,
|
|
21891
23194
|
targetId: target.id,
|
|
21892
23195
|
kind: "deleted",
|
|
21893
23196
|
ifMatchOid: entry.blobOid
|
|
@@ -21976,10 +23279,16 @@ async function pull(context, options = {}) {
|
|
|
21976
23279
|
}
|
|
21977
23280
|
const folders = targets.map((target) => target.folder);
|
|
21978
23281
|
const tracked = await context.git.tracked(folders) ?? /* @__PURE__ */ new Set();
|
|
23282
|
+
const acceptRemote = new Set(
|
|
23283
|
+
(options.acceptRemote ?? []).filter(
|
|
23284
|
+
(file) => path41.posix.normalize(file) === file && folders.some((folder) => file.startsWith(`${folder}/`))
|
|
23285
|
+
)
|
|
23286
|
+
);
|
|
21979
23287
|
const local = /* @__PURE__ */ new Map();
|
|
21980
23288
|
const relevant = /* @__PURE__ */ new Set([
|
|
21981
23289
|
...remote.map((file) => file.path),
|
|
21982
|
-
...Object.keys(lock.files)
|
|
23290
|
+
...Object.keys(lock.files),
|
|
23291
|
+
...acceptRemote
|
|
21983
23292
|
]);
|
|
21984
23293
|
for (const file of relevant) {
|
|
21985
23294
|
const hash = await hashLocalFile2(context.root, file);
|
|
@@ -21992,7 +23301,7 @@ async function pull(context, options = {}) {
|
|
|
21992
23301
|
local,
|
|
21993
23302
|
lock,
|
|
21994
23303
|
tracked,
|
|
21995
|
-
acceptRemote
|
|
23304
|
+
acceptRemote
|
|
21996
23305
|
});
|
|
21997
23306
|
await applyPull(context, lock, actions, result);
|
|
21998
23307
|
await resolveOutbox(context, lock);
|
|
@@ -22405,10 +23714,10 @@ function recordLines(record) {
|
|
|
22405
23714
|
const lines2 = [
|
|
22406
23715
|
` conflict in ${where}: nothing was written; these changed on the server since you last pulled:`
|
|
22407
23716
|
];
|
|
22408
|
-
for (const
|
|
22409
|
-
if (
|
|
23717
|
+
for (const path42 of record.paths) {
|
|
23718
|
+
if (path42.verdict !== void 0) {
|
|
22410
23719
|
lines2.push(
|
|
22411
|
-
` ${
|
|
23720
|
+
` ${path42.path}: ${path42.verdict.code}${path42.verdict.currentOid ? ` (now ${path42.verdict.currentOid.slice(0, 10)})` : ""}`
|
|
22412
23721
|
);
|
|
22413
23722
|
}
|
|
22414
23723
|
}
|
|
@@ -22420,7 +23729,7 @@ function recordLines(record) {
|
|
|
22420
23729
|
case "REFUSED":
|
|
22421
23730
|
return [
|
|
22422
23731
|
` refused in ${where}: ${record.detail ?? "see the server"}. Fix the files named below, then push again, or run \`fabric memory status --clear\` to dismiss.`,
|
|
22423
|
-
...record.paths.filter((
|
|
23732
|
+
...record.paths.filter((path42) => path42.verdict !== void 0).map((path42) => ` ${path42.path}: ${path42.verdict?.code}`)
|
|
22424
23733
|
];
|
|
22425
23734
|
case "FAILED":
|
|
22426
23735
|
return [
|
|
@@ -22450,9 +23759,9 @@ function pullLines(result) {
|
|
|
22450
23759
|
);
|
|
22451
23760
|
}
|
|
22452
23761
|
lines2.push(...keptLines(result));
|
|
22453
|
-
for (const
|
|
23762
|
+
for (const path42 of result.trackedByGit) {
|
|
22454
23763
|
lines2.push(
|
|
22455
|
-
`skipped ${
|
|
23764
|
+
`skipped ${path42}: your repository tracks it; use git for that file.`
|
|
22456
23765
|
);
|
|
22457
23766
|
}
|
|
22458
23767
|
for (const settled of result.settled) {
|