@fabricorg/cli 0.5.0 → 0.5.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 +897 -426
- package/dist/fabric.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/dist/fabric.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Command as Command21 } from "commander";
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "@fabricorg/cli",
|
|
9
|
-
version: "0.5.
|
|
9
|
+
version: "0.5.1",
|
|
10
10
|
description: "CLI for the Fabric AI platform",
|
|
11
11
|
keywords: [
|
|
12
12
|
"fabric",
|
|
@@ -235,10 +235,10 @@ var FabricHttpClient = class _FabricHttpClient {
|
|
|
235
235
|
* Inject default `org` / `personal` query params if the path doesn't already
|
|
236
236
|
* carry one. Resources that explicitly pass `?org=other` are untouched.
|
|
237
237
|
*/
|
|
238
|
-
buildUrl(
|
|
239
|
-
const url = `${this.baseUrl}/api/v1${
|
|
240
|
-
const hasOrg = /[?&]org=/.test(
|
|
241
|
-
const hasPersonal = /[?&]personal=/.test(
|
|
238
|
+
buildUrl(path29) {
|
|
239
|
+
const url = `${this.baseUrl}/api/v1${path29}`;
|
|
240
|
+
const hasOrg = /[?&]org=/.test(path29);
|
|
241
|
+
const hasPersonal = /[?&]personal=/.test(path29);
|
|
242
242
|
if (hasOrg || hasPersonal) {
|
|
243
243
|
return url;
|
|
244
244
|
}
|
|
@@ -251,11 +251,11 @@ var FabricHttpClient = class _FabricHttpClient {
|
|
|
251
251
|
if (additions.length === 0) {
|
|
252
252
|
return url;
|
|
253
253
|
}
|
|
254
|
-
const sep =
|
|
254
|
+
const sep = path29.includes("?") ? "&" : "?";
|
|
255
255
|
return `${url}${sep}${additions.join("&")}`;
|
|
256
256
|
}
|
|
257
|
-
async request(method,
|
|
258
|
-
const url = this.buildUrl(
|
|
257
|
+
async request(method, path29, options = {}) {
|
|
258
|
+
const url = this.buildUrl(path29);
|
|
259
259
|
const isMutating = MUTATING_METHODS.has(method);
|
|
260
260
|
const idempotencyKey = options.idempotencyKey ?? (isMutating ? randomId() : void 0);
|
|
261
261
|
const maxAttempts = (options.retry ?? this.retry).maxRetries + 1;
|
|
@@ -265,7 +265,7 @@ var FabricHttpClient = class _FabricHttpClient {
|
|
|
265
265
|
throw options.signal.reason;
|
|
266
266
|
}
|
|
267
267
|
const start = Date.now();
|
|
268
|
-
this.emit({ kind: "request", method, path:
|
|
268
|
+
this.emit({ kind: "request", method, path: path29, attempt: attempt2 });
|
|
269
269
|
try {
|
|
270
270
|
const result = await this.executeOnce(
|
|
271
271
|
method,
|
|
@@ -276,7 +276,7 @@ var FabricHttpClient = class _FabricHttpClient {
|
|
|
276
276
|
this.emit({
|
|
277
277
|
kind: "response",
|
|
278
278
|
method,
|
|
279
|
-
path:
|
|
279
|
+
path: path29,
|
|
280
280
|
attempt: attempt2,
|
|
281
281
|
status: 200,
|
|
282
282
|
durationMs: Date.now() - start
|
|
@@ -289,7 +289,7 @@ var FabricHttpClient = class _FabricHttpClient {
|
|
|
289
289
|
this.emit({
|
|
290
290
|
kind: retriable ? "retry" : "error",
|
|
291
291
|
method,
|
|
292
|
-
path:
|
|
292
|
+
path: path29,
|
|
293
293
|
attempt: attempt2,
|
|
294
294
|
status,
|
|
295
295
|
durationMs: Date.now() - start,
|
|
@@ -433,28 +433,28 @@ var FabricHttpClient = class _FabricHttpClient {
|
|
|
433
433
|
} catch {
|
|
434
434
|
}
|
|
435
435
|
}
|
|
436
|
-
get(
|
|
436
|
+
get(path29, options = {}) {
|
|
437
437
|
return this.request(
|
|
438
438
|
"GET",
|
|
439
|
-
|
|
439
|
+
path29,
|
|
440
440
|
options.signal ? { signal: options.signal } : {}
|
|
441
441
|
);
|
|
442
442
|
}
|
|
443
|
-
post(
|
|
444
|
-
return this.request("POST",
|
|
443
|
+
post(path29, body, options = {}) {
|
|
444
|
+
return this.request("POST", path29, {
|
|
445
445
|
body,
|
|
446
446
|
idempotencyKey: options.idempotencyKey,
|
|
447
447
|
retry: options.retry
|
|
448
448
|
});
|
|
449
449
|
}
|
|
450
|
-
patch(
|
|
451
|
-
return this.request("PATCH",
|
|
450
|
+
patch(path29, body, options = {}) {
|
|
451
|
+
return this.request("PATCH", path29, {
|
|
452
452
|
body,
|
|
453
453
|
idempotencyKey: options.idempotencyKey
|
|
454
454
|
});
|
|
455
455
|
}
|
|
456
|
-
put(
|
|
457
|
-
return this.request("PUT",
|
|
456
|
+
put(path29, body, options = {}) {
|
|
457
|
+
return this.request("PUT", path29, {
|
|
458
458
|
body,
|
|
459
459
|
idempotencyKey: options.idempotencyKey,
|
|
460
460
|
retry: options.retry
|
|
@@ -465,8 +465,8 @@ var FabricHttpClient = class _FabricHttpClient {
|
|
|
465
465
|
* delete names the path and the version in its body). Without `body`,
|
|
466
466
|
* nothing is sent, as before.
|
|
467
467
|
*/
|
|
468
|
-
delete(
|
|
469
|
-
return this.request("DELETE",
|
|
468
|
+
delete(path29, body, options = {}) {
|
|
469
|
+
return this.request("DELETE", path29, {
|
|
470
470
|
body,
|
|
471
471
|
idempotencyKey: options.idempotencyKey
|
|
472
472
|
});
|
|
@@ -1058,14 +1058,14 @@ function buildQuery3(opts) {
|
|
|
1058
1058
|
|
|
1059
1059
|
// ../sdk/src/resources/integrations.ts
|
|
1060
1060
|
var RESOURCE_BUILTINS = /* @__PURE__ */ new Set(["list", "constructor", "then"]);
|
|
1061
|
-
function makeDotPath(http, slug,
|
|
1062
|
-
const fn = (args) => http.post(`/integrations/${slug}/${
|
|
1061
|
+
function makeDotPath(http, slug, path29) {
|
|
1062
|
+
const fn = (args) => http.post(`/integrations/${slug}/${path29.join(".")}`, args ?? {});
|
|
1063
1063
|
return new Proxy(fn, {
|
|
1064
1064
|
get(target, prop) {
|
|
1065
1065
|
if (prop in target || typeof prop !== "string") {
|
|
1066
1066
|
return Reflect.get(target, prop);
|
|
1067
1067
|
}
|
|
1068
|
-
return makeDotPath(http, slug, [...
|
|
1068
|
+
return makeDotPath(http, slug, [...path29, prop]);
|
|
1069
1069
|
}
|
|
1070
1070
|
});
|
|
1071
1071
|
}
|
|
@@ -1743,10 +1743,21 @@ import path from "path";
|
|
|
1743
1743
|
import Conf from "conf";
|
|
1744
1744
|
|
|
1745
1745
|
// src/lib/exclusive-lock.ts
|
|
1746
|
-
import {
|
|
1746
|
+
import { randomUUID } from "crypto";
|
|
1747
|
+
import {
|
|
1748
|
+
closeSync,
|
|
1749
|
+
openSync,
|
|
1750
|
+
readFileSync,
|
|
1751
|
+
statSync,
|
|
1752
|
+
unlinkSync,
|
|
1753
|
+
writeSync
|
|
1754
|
+
} from "fs";
|
|
1747
1755
|
var ExclusiveLockBusyError = class extends Error {
|
|
1748
|
-
constructor() {
|
|
1749
|
-
super(
|
|
1756
|
+
constructor(abandoned = false) {
|
|
1757
|
+
super(
|
|
1758
|
+
abandoned ? "an abandoned lock must be removed manually" : "another process holds this lock"
|
|
1759
|
+
);
|
|
1760
|
+
this.abandoned = abandoned;
|
|
1750
1761
|
this.name = "ExclusiveLockBusyError";
|
|
1751
1762
|
}
|
|
1752
1763
|
};
|
|
@@ -1757,11 +1768,15 @@ function sleep2(ms) {
|
|
|
1757
1768
|
function isExistsError(error) {
|
|
1758
1769
|
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
1759
1770
|
}
|
|
1771
|
+
function isNotFoundError(error) {
|
|
1772
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1773
|
+
}
|
|
1760
1774
|
function takeOnce(lockPath2, staleMs) {
|
|
1761
1775
|
try {
|
|
1762
1776
|
const descriptor = openSync(lockPath2, "wx", 384);
|
|
1763
|
-
|
|
1764
|
-
|
|
1777
|
+
const owner = `${process.pid}:${randomUUID()}`;
|
|
1778
|
+
writeSync(descriptor, owner);
|
|
1779
|
+
return { descriptor, owner };
|
|
1765
1780
|
} catch (error) {
|
|
1766
1781
|
if (!isExistsError(error)) {
|
|
1767
1782
|
throw error;
|
|
@@ -1769,41 +1784,71 @@ function takeOnce(lockPath2, staleMs) {
|
|
|
1769
1784
|
}
|
|
1770
1785
|
try {
|
|
1771
1786
|
if (Date.now() - statSync(lockPath2).mtimeMs > staleMs) {
|
|
1772
|
-
|
|
1787
|
+
if (ownerIsAlive(readFileSync(lockPath2, "utf8"))) {
|
|
1788
|
+
return "busy";
|
|
1789
|
+
}
|
|
1790
|
+
return "abandoned";
|
|
1791
|
+
}
|
|
1792
|
+
} catch (error) {
|
|
1793
|
+
if (isNotFoundError(error)) {
|
|
1773
1794
|
return "retry";
|
|
1774
1795
|
}
|
|
1775
|
-
|
|
1776
|
-
return "retry";
|
|
1796
|
+
throw error;
|
|
1777
1797
|
}
|
|
1778
1798
|
return "busy";
|
|
1779
1799
|
}
|
|
1780
|
-
function
|
|
1781
|
-
|
|
1800
|
+
function ownerIsAlive(owner) {
|
|
1801
|
+
const match = /^(\d+)(?::[0-9a-f-]+)?$/.exec(owner);
|
|
1802
|
+
if (!match) {
|
|
1803
|
+
return false;
|
|
1804
|
+
}
|
|
1805
|
+
const pid = Number(match[1]);
|
|
1806
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
1807
|
+
return false;
|
|
1808
|
+
}
|
|
1782
1809
|
try {
|
|
1783
|
-
|
|
1810
|
+
process.kill(pid, 0);
|
|
1811
|
+
return true;
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
const code = error.code;
|
|
1814
|
+
return code === "EPERM" || code !== "ESRCH" && code !== "ENOENT";
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
function release(held, lockPath2) {
|
|
1818
|
+
closeSync(held.descriptor);
|
|
1819
|
+
try {
|
|
1820
|
+
if (readFileSync(lockPath2, "utf8") === held.owner) {
|
|
1821
|
+
unlinkSync(lockPath2);
|
|
1822
|
+
}
|
|
1784
1823
|
} catch {
|
|
1785
1824
|
}
|
|
1786
1825
|
}
|
|
1787
1826
|
async function withExclusiveLock(work, options) {
|
|
1788
1827
|
const { lockPath: lockPath2, staleMs } = options;
|
|
1789
1828
|
const deadline = Date.now() + options.waitMs;
|
|
1790
|
-
let
|
|
1791
|
-
while (
|
|
1829
|
+
let held;
|
|
1830
|
+
while (held === void 0) {
|
|
1792
1831
|
options.signal?.throwIfAborted();
|
|
1793
1832
|
const taken = takeOnce(lockPath2, staleMs);
|
|
1794
|
-
if (typeof taken === "
|
|
1795
|
-
|
|
1833
|
+
if (typeof taken === "object") {
|
|
1834
|
+
held = taken;
|
|
1835
|
+
} else if (taken === "abandoned") {
|
|
1836
|
+
throw new ExclusiveLockBusyError(true);
|
|
1796
1837
|
} else if (taken === "busy") {
|
|
1797
1838
|
if (Date.now() >= deadline) {
|
|
1798
1839
|
throw new ExclusiveLockBusyError();
|
|
1799
1840
|
}
|
|
1800
1841
|
await sleep2(options.pollMs ?? DEFAULT_POLL_MS);
|
|
1842
|
+
} else if (Date.now() >= deadline) {
|
|
1843
|
+
throw new ExclusiveLockBusyError();
|
|
1844
|
+
} else {
|
|
1845
|
+
await sleep2(options.pollMs ?? DEFAULT_POLL_MS);
|
|
1801
1846
|
}
|
|
1802
1847
|
}
|
|
1803
1848
|
try {
|
|
1804
1849
|
return await work();
|
|
1805
1850
|
} finally {
|
|
1806
|
-
release(
|
|
1851
|
+
release(held, lockPath2);
|
|
1807
1852
|
}
|
|
1808
1853
|
}
|
|
1809
1854
|
function sleepSync(ms) {
|
|
@@ -1812,22 +1857,28 @@ function sleepSync(ms) {
|
|
|
1812
1857
|
function withExclusiveLockSync(work, options) {
|
|
1813
1858
|
const { lockPath: lockPath2, staleMs } = options;
|
|
1814
1859
|
const deadline = Date.now() + options.waitMs;
|
|
1815
|
-
let
|
|
1816
|
-
while (
|
|
1860
|
+
let held;
|
|
1861
|
+
while (held === void 0) {
|
|
1817
1862
|
const taken = takeOnce(lockPath2, staleMs);
|
|
1818
|
-
if (typeof taken === "
|
|
1819
|
-
|
|
1863
|
+
if (typeof taken === "object") {
|
|
1864
|
+
held = taken;
|
|
1865
|
+
} else if (taken === "abandoned") {
|
|
1866
|
+
throw new ExclusiveLockBusyError(true);
|
|
1820
1867
|
} else if (taken === "busy") {
|
|
1821
1868
|
if (Date.now() >= deadline) {
|
|
1822
1869
|
throw new ExclusiveLockBusyError();
|
|
1823
1870
|
}
|
|
1824
1871
|
sleepSync(options.pollMs ?? DEFAULT_POLL_MS);
|
|
1872
|
+
} else if (Date.now() >= deadline) {
|
|
1873
|
+
throw new ExclusiveLockBusyError();
|
|
1874
|
+
} else {
|
|
1875
|
+
sleepSync(options.pollMs ?? DEFAULT_POLL_MS);
|
|
1825
1876
|
}
|
|
1826
1877
|
}
|
|
1827
1878
|
try {
|
|
1828
1879
|
return work();
|
|
1829
1880
|
} finally {
|
|
1830
|
-
release(
|
|
1881
|
+
release(held, lockPath2);
|
|
1831
1882
|
}
|
|
1832
1883
|
}
|
|
1833
1884
|
|
|
@@ -1935,7 +1986,7 @@ function update(change) {
|
|
|
1935
1986
|
);
|
|
1936
1987
|
} catch (error) {
|
|
1937
1988
|
throw error instanceof ExclusiveLockBusyError ? new Error(
|
|
1938
|
-
"Another fabric process is updating the configuration and did not finish. Try again."
|
|
1989
|
+
error.abandoned ? `A previous Fabric process left its configuration lock at ${JSON.stringify(lockPath2)}. Confirm it is no longer running, remove that lock, then try again.` : "Another fabric process is updating the configuration and did not finish. Try again."
|
|
1939
1990
|
) : error;
|
|
1940
1991
|
}
|
|
1941
1992
|
}
|
|
@@ -2257,8 +2308,8 @@ function sameOriginEndpoint(value, field2, issuer) {
|
|
|
2257
2308
|
}
|
|
2258
2309
|
function authorizationServerMetadataUrl(issuer) {
|
|
2259
2310
|
const url = new URL(issuer);
|
|
2260
|
-
const
|
|
2261
|
-
return `${url.origin}/.well-known/oauth-authorization-server${
|
|
2311
|
+
const path29 = url.pathname.replace(/\/+$/, "");
|
|
2312
|
+
return `${url.origin}/.well-known/oauth-authorization-server${path29}`;
|
|
2262
2313
|
}
|
|
2263
2314
|
async function discoverAuthorizationServer(baseUrl, options = {}) {
|
|
2264
2315
|
const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
|
|
@@ -2704,24 +2755,25 @@ var OAuthIssuerMismatchError = class extends Error {
|
|
|
2704
2755
|
}
|
|
2705
2756
|
};
|
|
2706
2757
|
var OAuthRefreshBusyError = class extends Error {
|
|
2707
|
-
constructor() {
|
|
2758
|
+
constructor(lockPath2) {
|
|
2708
2759
|
super(
|
|
2709
|
-
"Another fabric process is renewing the sign-in and did not finish. Try again."
|
|
2760
|
+
lockPath2 ? `A previous Fabric process left its sign-in lock at ${JSON.stringify(lockPath2)}. Confirm it is no longer running, remove that lock, then try again.` : "Another fabric process is renewing the sign-in and did not finish. Try again."
|
|
2710
2761
|
);
|
|
2711
2762
|
this.name = "OAuthRefreshBusyError";
|
|
2712
2763
|
}
|
|
2713
2764
|
};
|
|
2714
2765
|
async function withRefreshLock(work, options = {}) {
|
|
2766
|
+
const lockPath2 = options.lockPath ?? `${getConfigPath()}.refresh.lock`;
|
|
2715
2767
|
try {
|
|
2716
2768
|
return await withExclusiveLock(work, {
|
|
2717
|
-
lockPath:
|
|
2769
|
+
lockPath: lockPath2,
|
|
2718
2770
|
staleMs: LOCK_STALE_MS,
|
|
2719
2771
|
waitMs: options.waitMs ?? LOCK_WAIT_MS,
|
|
2720
2772
|
pollMs: LOCK_POLL_MS,
|
|
2721
2773
|
signal: options.signal
|
|
2722
2774
|
});
|
|
2723
2775
|
} catch (error) {
|
|
2724
|
-
throw error instanceof ExclusiveLockBusyError ? new OAuthRefreshBusyError() : error;
|
|
2776
|
+
throw error instanceof ExclusiveLockBusyError ? new OAuthRefreshBusyError(error.abandoned ? lockPath2 : void 0) : error;
|
|
2725
2777
|
}
|
|
2726
2778
|
}
|
|
2727
2779
|
function defaults(deps) {
|
|
@@ -3530,8 +3582,9 @@ function decideCheckoutVerdict(input) {
|
|
|
3530
3582
|
}
|
|
3531
3583
|
|
|
3532
3584
|
// src/lib/user-agent.ts
|
|
3585
|
+
var INSTRUCTIONS_STREAMING_CAPABILITY = "instructions-stream-v1";
|
|
3533
3586
|
function cliUserAgent(version = package_default.version, nodeVersion = process.versions.node, platform = process.platform) {
|
|
3534
|
-
return `fabric-cli/${version} (node/${nodeVersion}; ${platform})`;
|
|
3587
|
+
return `fabric-cli/${version} (node/${nodeVersion}; ${platform}; ${INSTRUCTIONS_STREAMING_CAPABILITY})`;
|
|
3535
3588
|
}
|
|
3536
3589
|
var MAX_NOTICE_CHARS = 300;
|
|
3537
3590
|
var notice = null;
|
|
@@ -4815,7 +4868,7 @@ async function assertWritableTarget(root, relativePath) {
|
|
|
4815
4868
|
}
|
|
4816
4869
|
return check;
|
|
4817
4870
|
}
|
|
4818
|
-
async function
|
|
4871
|
+
async function openFileSafely(root, relativePath) {
|
|
4819
4872
|
const check = await assertWritableTarget(root, relativePath);
|
|
4820
4873
|
if (!check.ok) {
|
|
4821
4874
|
throw new Error(`Refusing to sync: ${describeRejection(check)}.`);
|
|
@@ -4851,15 +4904,31 @@ async function readFileSafely(root, relativePath, options = {}) {
|
|
|
4851
4904
|
`Refusing to sync: ${relativePath} changed while it was being opened.`
|
|
4852
4905
|
);
|
|
4853
4906
|
}
|
|
4907
|
+
return { handle, stats: opened };
|
|
4908
|
+
} catch (error) {
|
|
4909
|
+
await handle.close().catch(() => {
|
|
4910
|
+
});
|
|
4911
|
+
throw error;
|
|
4912
|
+
}
|
|
4913
|
+
}
|
|
4914
|
+
async function readFileSafely(root, relativePath, options = {}) {
|
|
4915
|
+
const opened = await openFileSafely(root, relativePath);
|
|
4916
|
+
if (opened === null) {
|
|
4917
|
+
return null;
|
|
4918
|
+
}
|
|
4919
|
+
try {
|
|
4854
4920
|
if (options.maxBytes === void 0) {
|
|
4855
|
-
return {
|
|
4921
|
+
return {
|
|
4922
|
+
bytes: await opened.handle.readFile(),
|
|
4923
|
+
mode: opened.stats.mode
|
|
4924
|
+
};
|
|
4856
4925
|
}
|
|
4857
4926
|
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0) {
|
|
4858
4927
|
throw new Error(
|
|
4859
4928
|
"A safe file-read limit must be a non-negative integer."
|
|
4860
4929
|
);
|
|
4861
4930
|
}
|
|
4862
|
-
if (opened.size > options.maxBytes) {
|
|
4931
|
+
if (opened.stats.size > options.maxBytes) {
|
|
4863
4932
|
throw new Error(
|
|
4864
4933
|
`${relativePath} is too large to read safely (maximum ${options.maxBytes} bytes).`
|
|
4865
4934
|
);
|
|
@@ -4867,7 +4936,7 @@ async function readFileSafely(root, relativePath, options = {}) {
|
|
|
4867
4936
|
const buffer = Buffer.allocUnsafe(options.maxBytes + 1);
|
|
4868
4937
|
let total = 0;
|
|
4869
4938
|
while (total <= options.maxBytes) {
|
|
4870
|
-
const { bytesRead } = await handle.read(
|
|
4939
|
+
const { bytesRead } = await opened.handle.read(
|
|
4871
4940
|
buffer,
|
|
4872
4941
|
total,
|
|
4873
4942
|
options.maxBytes + 1 - total,
|
|
@@ -4883,9 +4952,44 @@ async function readFileSafely(root, relativePath, options = {}) {
|
|
|
4883
4952
|
`${relativePath} is too large to read safely (maximum ${options.maxBytes} bytes).`
|
|
4884
4953
|
);
|
|
4885
4954
|
}
|
|
4886
|
-
return { bytes: buffer.subarray(0, total), mode: opened.mode };
|
|
4955
|
+
return { bytes: buffer.subarray(0, total), mode: opened.stats.mode };
|
|
4887
4956
|
} finally {
|
|
4888
|
-
await handle.close();
|
|
4957
|
+
await opened.handle.close();
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4960
|
+
var FINGERPRINT_BUFFER_BYTES = 64 * 1024;
|
|
4961
|
+
async function fingerprintFileSafely(root, relativePath) {
|
|
4962
|
+
const opened = await openFileSafely(root, relativePath);
|
|
4963
|
+
if (opened === null) {
|
|
4964
|
+
return null;
|
|
4965
|
+
}
|
|
4966
|
+
try {
|
|
4967
|
+
const hash = createHash2("sha256");
|
|
4968
|
+
const buffer = Buffer.allocUnsafe(FINGERPRINT_BUFFER_BYTES);
|
|
4969
|
+
let size = 0;
|
|
4970
|
+
while (size <= opened.stats.size) {
|
|
4971
|
+
const remaining = opened.stats.size + 1 - size;
|
|
4972
|
+
const { bytesRead } = await opened.handle.read(
|
|
4973
|
+
buffer,
|
|
4974
|
+
0,
|
|
4975
|
+
Math.min(buffer.length, remaining),
|
|
4976
|
+
null
|
|
4977
|
+
);
|
|
4978
|
+
if (bytesRead === 0) {
|
|
4979
|
+
break;
|
|
4980
|
+
}
|
|
4981
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
4982
|
+
size += bytesRead;
|
|
4983
|
+
}
|
|
4984
|
+
const afterRead = await opened.handle.stat();
|
|
4985
|
+
if (size !== opened.stats.size || afterRead.size !== opened.stats.size || afterRead.dev !== opened.stats.dev || afterRead.ino !== opened.stats.ino) {
|
|
4986
|
+
throw new Error(
|
|
4987
|
+
`Refusing to sync: ${relativePath} changed while it was being read.`
|
|
4988
|
+
);
|
|
4989
|
+
}
|
|
4990
|
+
return { sha256: hash.digest("hex"), size, mode: opened.stats.mode };
|
|
4991
|
+
} finally {
|
|
4992
|
+
await opened.handle.close();
|
|
4889
4993
|
}
|
|
4890
4994
|
}
|
|
4891
4995
|
async function resolveExistingRoot(destination) {
|
|
@@ -5070,23 +5174,23 @@ function normalizeContextSourcePath(input) {
|
|
|
5070
5174
|
if (slashed.startsWith("/") || DRIVE_LETTER_PREFIX.test(slashed)) {
|
|
5071
5175
|
return { ok: false, reason: "absolute" };
|
|
5072
5176
|
}
|
|
5073
|
-
let
|
|
5074
|
-
while (
|
|
5075
|
-
|
|
5177
|
+
let path29 = slashed.replace(/\/{2,}/g, "/");
|
|
5178
|
+
while (path29.startsWith("./")) {
|
|
5179
|
+
path29 = path29.slice(2);
|
|
5076
5180
|
}
|
|
5077
|
-
if (
|
|
5181
|
+
if (path29.length === 0) {
|
|
5078
5182
|
return { ok: false, reason: "empty" };
|
|
5079
5183
|
}
|
|
5080
|
-
if (
|
|
5184
|
+
if (path29.endsWith("/")) {
|
|
5081
5185
|
return { ok: false, reason: "trailing-slash" };
|
|
5082
5186
|
}
|
|
5083
|
-
if (
|
|
5187
|
+
if (path29.split("/").some((segment) => segment === "." || segment === "..")) {
|
|
5084
5188
|
return { ok: false, reason: "dot-segment" };
|
|
5085
5189
|
}
|
|
5086
|
-
if (
|
|
5190
|
+
if (path29.length > MAX_CONTEXT_SOURCE_PATH_LENGTH) {
|
|
5087
5191
|
return { ok: false, reason: "too-long" };
|
|
5088
5192
|
}
|
|
5089
|
-
return { ok: true, sourcePath:
|
|
5193
|
+
return { ok: true, sourcePath: path29 };
|
|
5090
5194
|
}
|
|
5091
5195
|
|
|
5092
5196
|
// src/lib/context-sync/lock.ts
|
|
@@ -5974,9 +6078,9 @@ async function pushContextPlan(input) {
|
|
|
5974
6078
|
}
|
|
5975
6079
|
};
|
|
5976
6080
|
const pruneTargetsSoFar = () => {
|
|
5977
|
-
const keptByMoves = results.map(moveKeptOldPath).filter((
|
|
5978
|
-
return [.../* @__PURE__ */ new Set([...input.plan.removed, ...keptByMoves])].filter((
|
|
5979
|
-
const locked = input.lock?.files[
|
|
6081
|
+
const keptByMoves = results.map(moveKeptOldPath).filter((path29) => path29 !== null);
|
|
6082
|
+
return [.../* @__PURE__ */ new Set([...input.plan.removed, ...keptByMoves])].filter((path29) => {
|
|
6083
|
+
const locked = input.lock?.files[path29];
|
|
5980
6084
|
return locked !== void 0 && locked.state !== "duplicate";
|
|
5981
6085
|
}).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
5982
6086
|
};
|
|
@@ -6614,7 +6718,7 @@ function pruneSummary(results, targets) {
|
|
|
6614
6718
|
(r) => r.status === "deleted" || r.status === "already-gone"
|
|
6615
6719
|
).map((r) => r.sourcePath)
|
|
6616
6720
|
);
|
|
6617
|
-
const notDeleted = targets.filter((
|
|
6721
|
+
const notDeleted = targets.filter((path29) => !settled.has(path29)).length;
|
|
6618
6722
|
let text3 = `--prune: ${count("deleted")} deleted, ${count("already-gone")} already gone, ${notDeleted} not deleted`;
|
|
6619
6723
|
const first = results.find((r) => r.status === "delete-failed");
|
|
6620
6724
|
if (first && first.status === "delete-failed") {
|
|
@@ -6997,8 +7101,8 @@ function buildFramesCommand() {
|
|
|
6997
7101
|
}
|
|
6998
7102
|
|
|
6999
7103
|
// src/commands/instructions/index.ts
|
|
7000
|
-
import { readdir as
|
|
7001
|
-
import
|
|
7104
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
7105
|
+
import path28 from "path";
|
|
7002
7106
|
import { Command as Command12 } from "commander";
|
|
7003
7107
|
|
|
7004
7108
|
// src/lib/environment.ts
|
|
@@ -7122,6 +7226,11 @@ function runGit(cwd, args, deadline, options = {}) {
|
|
|
7122
7226
|
child = spawn2(
|
|
7123
7227
|
"git",
|
|
7124
7228
|
[
|
|
7229
|
+
// Windows Git can report modified long-path files from its file
|
|
7230
|
+
// system cache even when Git's diff is empty. This command-local
|
|
7231
|
+
// setting keeps inspection truthful without changing the user's
|
|
7232
|
+
// checkout configuration.
|
|
7233
|
+
...process.platform === "win32" ? ["-c", "core.fscache=false"] : [],
|
|
7125
7234
|
"-c",
|
|
7126
7235
|
"core.fsmonitor=false",
|
|
7127
7236
|
...options.write ? ["-c", "credential.interactive=never"] : [],
|
|
@@ -7541,6 +7650,77 @@ async function isClean(root, deadline) {
|
|
|
7541
7650
|
deadline
|
|
7542
7651
|
);
|
|
7543
7652
|
}
|
|
7653
|
+
async function trackedPaths(root, relativePaths, deadline) {
|
|
7654
|
+
if (relativePaths.length === 0) {
|
|
7655
|
+
return { kind: "ok", value: [] };
|
|
7656
|
+
}
|
|
7657
|
+
if (relativePaths.some(
|
|
7658
|
+
(relativePath) => relativePath === "" || relativePath.startsWith("-") || path7.isAbsolute(relativePath) || path7.win32.isAbsolute(relativePath) || relativePath.split(/[\\/]/).some((part) => part === "..")
|
|
7659
|
+
)) {
|
|
7660
|
+
return {
|
|
7661
|
+
kind: "unavailable",
|
|
7662
|
+
reason: "not paths git can be asked about"
|
|
7663
|
+
};
|
|
7664
|
+
}
|
|
7665
|
+
const result = await simple(
|
|
7666
|
+
root,
|
|
7667
|
+
["ls-files", "-z", "--", ...relativePaths],
|
|
7668
|
+
deadline
|
|
7669
|
+
);
|
|
7670
|
+
if (result.kind !== "ok") {
|
|
7671
|
+
return result;
|
|
7672
|
+
}
|
|
7673
|
+
if (result.value.code !== 0) {
|
|
7674
|
+
return {
|
|
7675
|
+
kind: "unavailable",
|
|
7676
|
+
reason: `git exited with status ${result.value.code}`
|
|
7677
|
+
};
|
|
7678
|
+
}
|
|
7679
|
+
return {
|
|
7680
|
+
kind: "ok",
|
|
7681
|
+
value: result.value.stdout.split("\0").filter((entry) => entry !== "")
|
|
7682
|
+
};
|
|
7683
|
+
}
|
|
7684
|
+
async function checkoutAppearsIncomplete(root, deadline) {
|
|
7685
|
+
const index = await simple(root, ["ls-files", "--stage", "-z"], deadline);
|
|
7686
|
+
if (index.kind !== "ok") {
|
|
7687
|
+
return index;
|
|
7688
|
+
}
|
|
7689
|
+
if (index.value.code !== 0) {
|
|
7690
|
+
return {
|
|
7691
|
+
kind: "unavailable",
|
|
7692
|
+
reason: `git exited with status ${index.value.code}`
|
|
7693
|
+
};
|
|
7694
|
+
}
|
|
7695
|
+
if (index.value.stdout !== "") {
|
|
7696
|
+
return { kind: "ok", value: false };
|
|
7697
|
+
}
|
|
7698
|
+
const [headTree, staged, untracked] = await Promise.all([
|
|
7699
|
+
simple(root, ["ls-tree", "-r", "-z", "--name-only", "HEAD"], deadline),
|
|
7700
|
+
simple(root, ["diff", "--cached", "--quiet", "--exit-code"], deadline),
|
|
7701
|
+
simple(
|
|
7702
|
+
root,
|
|
7703
|
+
["ls-files", "--others", "--exclude-standard", "-z"],
|
|
7704
|
+
deadline
|
|
7705
|
+
)
|
|
7706
|
+
]);
|
|
7707
|
+
if (headTree.kind !== "ok") {
|
|
7708
|
+
return headTree;
|
|
7709
|
+
}
|
|
7710
|
+
if (staged.kind !== "ok") {
|
|
7711
|
+
return staged;
|
|
7712
|
+
}
|
|
7713
|
+
if (untracked.kind !== "ok") {
|
|
7714
|
+
return untracked;
|
|
7715
|
+
}
|
|
7716
|
+
if (headTree.value.code !== 0 || staged.value.code !== 0 && staged.value.code !== 1 || untracked.value.code !== 0) {
|
|
7717
|
+
return { kind: "unavailable", reason: "git answered unexpectedly" };
|
|
7718
|
+
}
|
|
7719
|
+
return {
|
|
7720
|
+
kind: "ok",
|
|
7721
|
+
value: headTree.value.stdout !== "" && staged.value.code === 1 && untracked.value.stdout !== ""
|
|
7722
|
+
};
|
|
7723
|
+
}
|
|
7544
7724
|
async function porcelainIsEmpty(root, args, deadline) {
|
|
7545
7725
|
const result = await simple(
|
|
7546
7726
|
root,
|
|
@@ -7767,6 +7947,11 @@ function cloneFailureOf(stderr) {
|
|
|
7767
7947
|
if (/Remote branch .* not found/i.test(stderr)) {
|
|
7768
7948
|
return "missing-ref";
|
|
7769
7949
|
}
|
|
7950
|
+
if (/Filename too long|unable to create file|unable to checkout working tree/i.test(
|
|
7951
|
+
stderr
|
|
7952
|
+
)) {
|
|
7953
|
+
return "checkout";
|
|
7954
|
+
}
|
|
7770
7955
|
if (/could not read (Username|Password)|terminal prompts disabled|Authentication failed|Permission denied|HTTP (401|403)|error: (401|403)|returned error: (401|403)|Repository not found|access denied/i.test(
|
|
7771
7956
|
stderr
|
|
7772
7957
|
)) {
|
|
@@ -7788,7 +7973,22 @@ async function cloneInto(dir, url, ref, deadline) {
|
|
|
7788
7973
|
}
|
|
7789
7974
|
const result = await runGit(
|
|
7790
7975
|
dir,
|
|
7791
|
-
[
|
|
7976
|
+
[
|
|
7977
|
+
...process.platform === "win32" ? ["-c", "core.longpaths=true", "-c", "core.fscache=false"] : [],
|
|
7978
|
+
"clone",
|
|
7979
|
+
"--quiet",
|
|
7980
|
+
...process.platform === "win32" ? [
|
|
7981
|
+
"--config",
|
|
7982
|
+
"core.longpaths=true",
|
|
7983
|
+
"--config",
|
|
7984
|
+
"core.fscache=false"
|
|
7985
|
+
] : [],
|
|
7986
|
+
"--branch",
|
|
7987
|
+
ref,
|
|
7988
|
+
"--",
|
|
7989
|
+
url,
|
|
7990
|
+
"."
|
|
7991
|
+
],
|
|
7792
7992
|
deadline,
|
|
7793
7993
|
{ write: true }
|
|
7794
7994
|
);
|
|
@@ -7879,16 +8079,19 @@ var LINES = {
|
|
|
7879
8079
|
"clone-needs-project": () => "--clone <folder> needs --project <id>: there is no checkout yet to find the project from.",
|
|
7880
8080
|
"clone-needs-repository": () => "--clone needs a project whose instructions come from a git repository; this project's are uploaded, so there is nothing to clone.",
|
|
7881
8081
|
"clone-folder-in-use": () => "That folder already exists and is not empty, so nothing was cloned into it. Pick a folder that does not exist yet, or run init inside it when it already is the clone.",
|
|
8082
|
+
"clone-checkout-needs-repair": () => "This folder appears to be an incomplete Git checkout. Fabric left it untouched. Inspect its staged and untracked files with git status, then repair or remove the checkout yourself before running init again.",
|
|
7882
8083
|
"clone-failed": ({ repo, ref, host, url, reason, login }) => {
|
|
7883
8084
|
switch (reason) {
|
|
7884
8085
|
case "auth":
|
|
7885
|
-
return `Could not clone ${repo}: git has no credentials for ${shown(host)}. Run: ${login}`;
|
|
8086
|
+
return `Could not clone ${repo}: git has no credentials for ${shown(host)}. The Fabric MCP server was not registered. Run: ${login}`;
|
|
7886
8087
|
case "network":
|
|
7887
|
-
return `Could not clone ${repo}: ${shown(host)} did not answer. Check your network and try again.`;
|
|
8088
|
+
return `Could not clone ${repo}: ${shown(host)} did not answer. The Fabric MCP server was not registered. Check your network and try again.`;
|
|
7888
8089
|
case "missing-ref":
|
|
7889
|
-
return `Could not clone ${repo}: it has no branch ${shown(ref)}.`;
|
|
8090
|
+
return `Could not clone ${repo}: it has no branch ${shown(ref)}. The Fabric MCP server was not registered.`;
|
|
8091
|
+
case "checkout":
|
|
8092
|
+
return `Could not finish checking out ${repo}. Fabric did not change the partial checkout or register its MCP server. Inspect it with git status, then repair or remove it yourself before trying again.`;
|
|
7890
8093
|
case "other":
|
|
7891
|
-
return `Could not clone ${repo}. Run: git clone -- ${url} to see why.`;
|
|
8094
|
+
return `Could not clone ${repo}. The Fabric MCP server was not registered. Run: git clone -- ${url} to see why.`;
|
|
7892
8095
|
default:
|
|
7893
8096
|
return reason;
|
|
7894
8097
|
}
|
|
@@ -7964,6 +8167,7 @@ var LINES = {
|
|
|
7964
8167
|
}
|
|
7965
8168
|
},
|
|
7966
8169
|
"ff-locked": () => `${HOOK_PREFIX}: another fabric process is updating this checkout; nothing was changed.`,
|
|
8170
|
+
"ff-abandoned-lock": ({ lockPath: lockPath2 }) => `${HOOK_PREFIX}: a previous Fabric process left its lock at ${JSON.stringify(lockPath2)}; nothing was changed. Confirm it is no longer running, remove that lock, then run the hook again.`,
|
|
7967
8171
|
"ff-fabric-lags": ({ reason, ref, sha7 }) => `${HOOK_PREFIX}: ${ref} is at ${sha7}; Fabric's copy is behind (${LAG_TEXT[reason]}).`,
|
|
7968
8172
|
"class-foreign": ({ repo }) => `${HOOK_PREFIX}: no remote of this checkout fetches from ${repo}; nothing was checked.`,
|
|
7969
8173
|
"class-ambiguous": ({ repo, remotes: remotes2 }) => `${HOOK_PREFIX}: remotes ${remotes2.join(", ")} all fetch from ${repo}; nothing was checked. Run: ${fabricCommand(`instructions init --remote ${remotes2[0] ?? "origin"}`)}`,
|
|
@@ -8017,6 +8221,7 @@ var EXIT_CODES = {
|
|
|
8017
8221
|
"clone-needs-project": 2,
|
|
8018
8222
|
"clone-needs-repository": 7,
|
|
8019
8223
|
"clone-folder-in-use": 7,
|
|
8224
|
+
"clone-checkout-needs-repair": 7,
|
|
8020
8225
|
"clone-failed": ({ reason }) => reason === "auth" ? 3 : reason === "missing-ref" ? 7 : 1,
|
|
8021
8226
|
"no-clone-url": 7,
|
|
8022
8227
|
"lock-removed": 0,
|
|
@@ -8030,6 +8235,7 @@ var EXIT_CODES = {
|
|
|
8030
8235
|
"ff-fetch-failed": 0,
|
|
8031
8236
|
"ff-merge-failed": 0,
|
|
8032
8237
|
"ff-locked": 0,
|
|
8238
|
+
"ff-abandoned-lock": 0,
|
|
8033
8239
|
"ff-fabric-lags": 0,
|
|
8034
8240
|
"class-foreign": 7,
|
|
8035
8241
|
"class-ambiguous": 7,
|
|
@@ -8899,6 +9105,9 @@ async function readRegistration(input) {
|
|
|
8899
9105
|
}
|
|
8900
9106
|
|
|
8901
9107
|
// src/lib/instructions/hook.ts
|
|
9108
|
+
import { createHash as createHash4 } from "crypto";
|
|
9109
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
9110
|
+
import { tmpdir } from "os";
|
|
8902
9111
|
import path11 from "path";
|
|
8903
9112
|
|
|
8904
9113
|
// src/lib/instructions/identifiers.ts
|
|
@@ -8917,6 +9126,8 @@ var CLAUDE_SETTINGS_POSIX_PATH = ".claude/settings.local.json";
|
|
|
8917
9126
|
var CODEX_HOOKS_POSIX_PATH = ".codex/hooks.json";
|
|
8918
9127
|
var HOOK_TIMEOUT_SECONDS = 15;
|
|
8919
9128
|
var MAX_HOOK_CONFIG_BYTES = 1024 * 1024;
|
|
9129
|
+
var HOOK_LOCK_STALE_MS = 3e4;
|
|
9130
|
+
var HOOK_LOCK_WAIT_MS = 5e3;
|
|
8920
9131
|
var HOOK_SUBCOMMANDS = /* @__PURE__ */ new Set(["check", "sync", "lesson-prompt"]);
|
|
8921
9132
|
var SESSION_START_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
8922
9133
|
"check",
|
|
@@ -8926,6 +9137,26 @@ var STOP_SUBCOMMANDS = /* @__PURE__ */ new Set(["lesson-prompt"]);
|
|
|
8926
9137
|
function subcommandsForEvent(event) {
|
|
8927
9138
|
return event === "Stop" ? STOP_SUBCOMMANDS : SESSION_START_SUBCOMMANDS;
|
|
8928
9139
|
}
|
|
9140
|
+
async function withHookConfigLock(root, posixPath, work) {
|
|
9141
|
+
const directory = path11.join(tmpdir(), "fabric-instructions-hook-locks");
|
|
9142
|
+
await mkdir2(directory, { recursive: true });
|
|
9143
|
+
const identity = createHash4("sha256").update(path11.resolve(root)).update("\0").update(posixPath).digest("hex");
|
|
9144
|
+
const lockPath2 = path11.join(directory, `${identity}.lock`);
|
|
9145
|
+
try {
|
|
9146
|
+
return await withExclusiveLock(work, {
|
|
9147
|
+
lockPath: lockPath2,
|
|
9148
|
+
staleMs: HOOK_LOCK_STALE_MS,
|
|
9149
|
+
waitMs: HOOK_LOCK_WAIT_MS
|
|
9150
|
+
});
|
|
9151
|
+
} catch (error) {
|
|
9152
|
+
if (error instanceof ExclusiveLockBusyError && error.abandoned) {
|
|
9153
|
+
throw new Error(
|
|
9154
|
+
`A previous Fabric process left the hook configuration lock at ${JSON.stringify(lockPath2)}. Confirm it is no longer running, remove that lock, then run init again.`
|
|
9155
|
+
);
|
|
9156
|
+
}
|
|
9157
|
+
throw error;
|
|
9158
|
+
}
|
|
9159
|
+
}
|
|
8929
9160
|
function assertCommandIdentifiers(projectId, org) {
|
|
8930
9161
|
if (!isIdentifier(projectId) || org !== void 0 && !isIdentifier(org)) {
|
|
8931
9162
|
throw new Error(
|
|
@@ -9043,85 +9274,89 @@ function pruneMatching(groups, projectId, subcommands) {
|
|
|
9043
9274
|
}
|
|
9044
9275
|
async function mergeCommandHook(input) {
|
|
9045
9276
|
const hookTarget = hookTargetFor(input.tool ?? "claude-code");
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9277
|
+
return withHookConfigLock(input.root, hookTarget.posixPath, async () => {
|
|
9278
|
+
const settingsPath = path11.join(input.root, hookTarget.relativePath);
|
|
9279
|
+
const { settings, existingRaw } = await readHookConfig(
|
|
9280
|
+
input.root,
|
|
9281
|
+
hookTarget
|
|
9282
|
+
);
|
|
9283
|
+
const { hooks, groups } = readEventGroups(
|
|
9284
|
+
settingsPath,
|
|
9285
|
+
settings,
|
|
9286
|
+
input.event
|
|
9287
|
+
);
|
|
9288
|
+
const replacement = {
|
|
9289
|
+
type: "command",
|
|
9290
|
+
command: input.command,
|
|
9291
|
+
timeout: HOOK_TIMEOUT_SECONDS
|
|
9292
|
+
};
|
|
9293
|
+
const { pruned, removedCount } = pruneMatching(
|
|
9294
|
+
groups,
|
|
9295
|
+
input.projectId,
|
|
9296
|
+
subcommandsForEvent(input.event)
|
|
9297
|
+
);
|
|
9298
|
+
pruned.push({ hooks: [replacement] });
|
|
9299
|
+
const next = {
|
|
9300
|
+
...settings,
|
|
9301
|
+
hooks: { ...hooks, [input.event]: pruned }
|
|
9302
|
+
};
|
|
9303
|
+
await writeFileSafely({
|
|
9304
|
+
root: input.root,
|
|
9305
|
+
relativePath: hookTarget.posixPath,
|
|
9306
|
+
bytes: new TextEncoder().encode(
|
|
9307
|
+
`${JSON.stringify(next, null, 2).replace(/\r\n/g, "\n")}
|
|
9076
9308
|
`
|
|
9077
|
-
|
|
9309
|
+
)
|
|
9310
|
+
});
|
|
9311
|
+
return {
|
|
9312
|
+
settingsPath,
|
|
9313
|
+
command: input.command,
|
|
9314
|
+
createdFile: existingRaw === null,
|
|
9315
|
+
replacedCount: removedCount
|
|
9316
|
+
};
|
|
9078
9317
|
});
|
|
9079
|
-
return {
|
|
9080
|
-
settingsPath,
|
|
9081
|
-
command: input.command,
|
|
9082
|
-
createdFile: existingRaw === null,
|
|
9083
|
-
replacedCount: removedCount
|
|
9084
|
-
};
|
|
9085
9318
|
}
|
|
9086
9319
|
async function mergeSessionStartHook(input) {
|
|
9087
9320
|
return mergeCommandHook({ ...input, event: "SessionStart" });
|
|
9088
9321
|
}
|
|
9089
9322
|
async function removeCommandHook(input) {
|
|
9090
9323
|
const hookTarget = hookTargetFor(input.tool ?? "claude-code");
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9324
|
+
return withHookConfigLock(input.root, hookTarget.posixPath, async () => {
|
|
9325
|
+
const settingsPath = path11.join(input.root, hookTarget.relativePath);
|
|
9326
|
+
const { settings, existingRaw } = await readHookConfig(
|
|
9327
|
+
input.root,
|
|
9328
|
+
hookTarget
|
|
9329
|
+
);
|
|
9330
|
+
if (existingRaw === null) {
|
|
9331
|
+
return { settingsPath, changed: false };
|
|
9332
|
+
}
|
|
9333
|
+
const { hooks, groups } = readEventGroups(
|
|
9334
|
+
settingsPath,
|
|
9335
|
+
settings,
|
|
9336
|
+
input.event
|
|
9337
|
+
);
|
|
9338
|
+
const { pruned, removedCount } = pruneMatching(
|
|
9339
|
+
groups,
|
|
9340
|
+
input.projectId,
|
|
9341
|
+
/* @__PURE__ */ new Set([input.subcommand])
|
|
9342
|
+
);
|
|
9343
|
+
if (removedCount === 0) {
|
|
9344
|
+
return { settingsPath, changed: false };
|
|
9345
|
+
}
|
|
9346
|
+
const next = {
|
|
9347
|
+
...settings,
|
|
9348
|
+
hooks: { ...hooks, [input.event]: pruned }
|
|
9349
|
+
};
|
|
9350
|
+
await writeFileSafely({
|
|
9351
|
+
root: input.root,
|
|
9352
|
+
relativePath: hookTarget.posixPath,
|
|
9353
|
+
bytes: new TextEncoder().encode(
|
|
9354
|
+
`${JSON.stringify(next, null, 2).replace(/\r\n/g, "\n")}
|
|
9121
9355
|
`
|
|
9122
|
-
|
|
9356
|
+
)
|
|
9357
|
+
});
|
|
9358
|
+
return { settingsPath, changed: true };
|
|
9123
9359
|
});
|
|
9124
|
-
return { settingsPath, changed: true };
|
|
9125
9360
|
}
|
|
9126
9361
|
async function findSessionStartHooks(input) {
|
|
9127
9362
|
const hookTarget = hookTargetFor(input.tool);
|
|
@@ -9292,6 +9527,16 @@ var TOOL_COMMAND = {
|
|
|
9292
9527
|
"claude-code": "claude",
|
|
9293
9528
|
codex: "codex"
|
|
9294
9529
|
};
|
|
9530
|
+
function mcpRegistrationComplete(results) {
|
|
9531
|
+
return results.every(
|
|
9532
|
+
(result) => result.outcome.kind === "registered" || result.outcome.kind === "replaced" || result.outcome.kind === "already"
|
|
9533
|
+
);
|
|
9534
|
+
}
|
|
9535
|
+
function mcpAuthenticationPending(results) {
|
|
9536
|
+
return results.some(
|
|
9537
|
+
(result) => result.login === "printed" || result.login === "not-finished"
|
|
9538
|
+
);
|
|
9539
|
+
}
|
|
9295
9540
|
var SERVER_ID_SUFFIX_LENGTH = 6;
|
|
9296
9541
|
function codexServerName(projectId) {
|
|
9297
9542
|
const suffix = projectId.toLowerCase().replace(/[^a-z0-9]+/g, "").slice(-SERVER_ID_SUFFIX_LENGTH);
|
|
@@ -9795,9 +10040,6 @@ function createAgentRunner(input) {
|
|
|
9795
10040
|
import { createHash as createHash5 } from "crypto";
|
|
9796
10041
|
import { chmod } from "fs/promises";
|
|
9797
10042
|
|
|
9798
|
-
// src/lib/instructions/plan.ts
|
|
9799
|
-
import { createHash as createHash4 } from "crypto";
|
|
9800
|
-
|
|
9801
10043
|
// src/lib/instructions/lock.ts
|
|
9802
10044
|
import { readFile as readFile3 } from "fs/promises";
|
|
9803
10045
|
import path14 from "path";
|
|
@@ -9970,8 +10212,7 @@ function sourceProblem(value) {
|
|
|
9970
10212
|
|
|
9971
10213
|
// src/lib/instructions/plan.ts
|
|
9972
10214
|
async function hashLocalFile(root, relativePath) {
|
|
9973
|
-
|
|
9974
|
-
return read === null ? null : createHash4("sha256").update(read.bytes).digest("hex");
|
|
10215
|
+
return (await fingerprintFileSafely(root, relativePath))?.sha256 ?? null;
|
|
9975
10216
|
}
|
|
9976
10217
|
function lockPaths(lock) {
|
|
9977
10218
|
return lock === null ? [] : Object.keys(lock.files);
|
|
@@ -10012,9 +10253,9 @@ async function findLedgerDrift(input) {
|
|
|
10012
10253
|
assertSafeLockPaths(lockPaths(input.lock));
|
|
10013
10254
|
const drifted = [];
|
|
10014
10255
|
for (const [lockedPath, locked] of Object.entries(input.lock.files)) {
|
|
10015
|
-
let
|
|
10256
|
+
let fingerprint;
|
|
10016
10257
|
try {
|
|
10017
|
-
|
|
10258
|
+
fingerprint = await fingerprintFileSafely(input.root, lockedPath);
|
|
10018
10259
|
} catch (error) {
|
|
10019
10260
|
drifted.push({
|
|
10020
10261
|
path: lockedPath,
|
|
@@ -10024,7 +10265,7 @@ async function findLedgerDrift(input) {
|
|
|
10024
10265
|
});
|
|
10025
10266
|
continue;
|
|
10026
10267
|
}
|
|
10027
|
-
if (
|
|
10268
|
+
if (fingerprint === null) {
|
|
10028
10269
|
drifted.push({
|
|
10029
10270
|
path: lockedPath,
|
|
10030
10271
|
reason: "missing",
|
|
@@ -10033,7 +10274,7 @@ async function findLedgerDrift(input) {
|
|
|
10033
10274
|
});
|
|
10034
10275
|
continue;
|
|
10035
10276
|
}
|
|
10036
|
-
const actual =
|
|
10277
|
+
const actual = fingerprint.sha256;
|
|
10037
10278
|
if (actual !== locked.sha256) {
|
|
10038
10279
|
drifted.push({
|
|
10039
10280
|
path: lockedPath,
|
|
@@ -10043,11 +10284,11 @@ async function findLedgerDrift(input) {
|
|
|
10043
10284
|
});
|
|
10044
10285
|
continue;
|
|
10045
10286
|
}
|
|
10046
|
-
if (process.platform !== "win32" && locked.mode !== null && (
|
|
10287
|
+
if (process.platform !== "win32" && locked.mode !== null && (fingerprint.mode & 4095) !== (locked.mode & 4095)) {
|
|
10047
10288
|
drifted.push({
|
|
10048
10289
|
path: lockedPath,
|
|
10049
10290
|
reason: "mode",
|
|
10050
|
-
detail: `mode ${(
|
|
10291
|
+
detail: `mode ${(fingerprint.mode & 4095).toString(8)}, published as ${(locked.mode & 4095).toString(8)}`,
|
|
10051
10292
|
kept: locked.kept === true
|
|
10052
10293
|
});
|
|
10053
10294
|
}
|
|
@@ -10207,7 +10448,7 @@ async function applyPlan(input) {
|
|
|
10207
10448
|
`Refusing to sync: ${entry.path} asks for file mode ${(entry.mode ?? 0).toString(8)}, and only 644 and 755 are accepted. Nothing was written.`
|
|
10208
10449
|
);
|
|
10209
10450
|
}
|
|
10210
|
-
const bytes = contents.get(entry.path);
|
|
10451
|
+
const bytes = await contents.get(entry.path);
|
|
10211
10452
|
if (!bytes) {
|
|
10212
10453
|
throw new Error(
|
|
10213
10454
|
`The downloaded bundle is missing ${entry.path}. Nothing was written.`
|
|
@@ -10233,7 +10474,10 @@ async function applyPlan(input) {
|
|
|
10233
10474
|
continue;
|
|
10234
10475
|
}
|
|
10235
10476
|
}
|
|
10236
|
-
const bytes = contents.get(entry.path);
|
|
10477
|
+
const bytes = await contents.get(entry.path);
|
|
10478
|
+
if (!bytes) {
|
|
10479
|
+
throw new Error(`The downloaded bundle is missing ${entry.path}.`);
|
|
10480
|
+
}
|
|
10237
10481
|
await writeFileSafely({
|
|
10238
10482
|
root,
|
|
10239
10483
|
relativePath: entry.path,
|
|
@@ -10414,7 +10658,7 @@ function extractBundle(archive, wanted) {
|
|
|
10414
10658
|
}
|
|
10415
10659
|
|
|
10416
10660
|
// src/lib/instructions/clone-target.ts
|
|
10417
|
-
import { lstat as lstat5
|
|
10661
|
+
import { lstat as lstat5 } from "fs/promises";
|
|
10418
10662
|
import path15 from "path";
|
|
10419
10663
|
async function resolveCloneFolder(base, folder) {
|
|
10420
10664
|
const target = path15.resolve(base, folder);
|
|
@@ -10427,23 +10671,117 @@ async function resolveCloneFolder(base, folder) {
|
|
|
10427
10671
|
if (stats === null) {
|
|
10428
10672
|
return target;
|
|
10429
10673
|
}
|
|
10430
|
-
if (!stats.isDirectory()
|
|
10674
|
+
if (!stats.isDirectory()) {
|
|
10431
10675
|
throw outcomeFailure("clone-folder-in-use", {});
|
|
10432
10676
|
}
|
|
10433
10677
|
return target;
|
|
10434
10678
|
}
|
|
10435
10679
|
|
|
10436
10680
|
// src/lib/instructions/doctor.ts
|
|
10437
|
-
import { createHash as
|
|
10681
|
+
import { createHash as createHash9 } from "crypto";
|
|
10438
10682
|
import { stat as stat4 } from "fs/promises";
|
|
10439
|
-
import
|
|
10683
|
+
import path17 from "path";
|
|
10440
10684
|
|
|
10441
|
-
// src/lib/instructions/
|
|
10685
|
+
// src/lib/instructions/file-downloads.ts
|
|
10442
10686
|
import { createHash as createHash6 } from "crypto";
|
|
10687
|
+
var PER_FILE_MAX_WRITES = 100;
|
|
10688
|
+
var URL_REQUEST_CHUNK = 200;
|
|
10689
|
+
var DOWNLOAD_CONCURRENCY = 8;
|
|
10690
|
+
var PublishedChangedError = class extends Error {
|
|
10691
|
+
constructor() {
|
|
10692
|
+
super("The published version changed while this command ran.");
|
|
10693
|
+
this.name = "PublishedChangedError";
|
|
10694
|
+
}
|
|
10695
|
+
};
|
|
10696
|
+
async function fetchFilesByUrl(options) {
|
|
10697
|
+
const contents = /* @__PURE__ */ new Map();
|
|
10698
|
+
await downloadFilesByUrl(options, async (file, bytes) => {
|
|
10699
|
+
contents.set(file.path, bytes);
|
|
10700
|
+
});
|
|
10701
|
+
return contents;
|
|
10702
|
+
}
|
|
10703
|
+
async function downloadFilesByUrl(options, consume) {
|
|
10704
|
+
for (let i = 0; i < options.files.length; i += URL_REQUEST_CHUNK) {
|
|
10705
|
+
const files = options.files.slice(i, i + URL_REQUEST_CHUNK);
|
|
10706
|
+
const wanted = new Set(files.map((file) => file.path));
|
|
10707
|
+
const urls = /* @__PURE__ */ new Map();
|
|
10708
|
+
const paths = files.map((file) => file.path);
|
|
10709
|
+
let response;
|
|
10710
|
+
try {
|
|
10711
|
+
response = await options.client.instructions.createFileDownloadUrls(
|
|
10712
|
+
options.projectId,
|
|
10713
|
+
{ digest: options.digest, paths },
|
|
10714
|
+
{ org: options.org }
|
|
10715
|
+
);
|
|
10716
|
+
} catch (error) {
|
|
10717
|
+
if (error instanceof FabricError && error.status === 409 && error.code === "PUBLISHED_CHANGED") {
|
|
10718
|
+
throw new PublishedChangedError();
|
|
10719
|
+
}
|
|
10720
|
+
throw error;
|
|
10721
|
+
}
|
|
10722
|
+
if (response.digest !== options.digest) {
|
|
10723
|
+
throw new PublishedChangedError();
|
|
10724
|
+
}
|
|
10725
|
+
for (const file of response.files) {
|
|
10726
|
+
if (!wanted.has(file.path) || urls.has(file.path)) {
|
|
10727
|
+
throw new Error(
|
|
10728
|
+
"The server returned a file that was not asked for. Nothing was written."
|
|
10729
|
+
);
|
|
10730
|
+
}
|
|
10731
|
+
urls.set(file.path, file.url);
|
|
10732
|
+
}
|
|
10733
|
+
if (urls.size !== wanted.size) {
|
|
10734
|
+
throw new Error(
|
|
10735
|
+
`The server returned ${urls.size} of ${wanted.size} requested files. Nothing was written.`
|
|
10736
|
+
);
|
|
10737
|
+
}
|
|
10738
|
+
const queue = files.values();
|
|
10739
|
+
const failures = [];
|
|
10740
|
+
const worker = async () => {
|
|
10741
|
+
while (failures.length === 0) {
|
|
10742
|
+
const next = queue.next();
|
|
10743
|
+
if (next.done) {
|
|
10744
|
+
return;
|
|
10745
|
+
}
|
|
10746
|
+
const file = next.value;
|
|
10747
|
+
try {
|
|
10748
|
+
const bytes = await fetchBundle(urls.get(file.path) ?? "", {
|
|
10749
|
+
timeoutMs: options.timeoutMs,
|
|
10750
|
+
// The manifest's size, not the response's: the read is
|
|
10751
|
+
// abandoned the moment it passes what the plan expects.
|
|
10752
|
+
maxBytes: file.size,
|
|
10753
|
+
signal: options.signal,
|
|
10754
|
+
fetchImpl: options.fetchImpl
|
|
10755
|
+
});
|
|
10756
|
+
if (bytes.length !== file.size || createHash6("sha256").update(bytes).digest("hex") !== file.sha256) {
|
|
10757
|
+
throw new Error(
|
|
10758
|
+
`A downloaded file does not match the manifest (${file.path}). Nothing was written.`
|
|
10759
|
+
);
|
|
10760
|
+
}
|
|
10761
|
+
await consume(file, bytes);
|
|
10762
|
+
} catch (error) {
|
|
10763
|
+
failures.push(error);
|
|
10764
|
+
}
|
|
10765
|
+
}
|
|
10766
|
+
};
|
|
10767
|
+
await Promise.all(
|
|
10768
|
+
Array.from(
|
|
10769
|
+
{ length: Math.min(DOWNLOAD_CONCURRENCY, files.length) },
|
|
10770
|
+
() => worker()
|
|
10771
|
+
)
|
|
10772
|
+
);
|
|
10773
|
+
if (failures.length > 0) {
|
|
10774
|
+
throw failures[0];
|
|
10775
|
+
}
|
|
10776
|
+
}
|
|
10777
|
+
}
|
|
10778
|
+
|
|
10779
|
+
// src/lib/instructions/manifest.ts
|
|
10780
|
+
import { createHash as createHash7 } from "crypto";
|
|
10443
10781
|
var SHA256_HEX3 = /^[0-9a-f]{64}$/;
|
|
10444
10782
|
var MANIFEST_LIMITS = {
|
|
10445
10783
|
maxFiles: 5e3,
|
|
10446
|
-
maxTotalBytes:
|
|
10784
|
+
maxTotalBytes: 2147483647,
|
|
10447
10785
|
maxFileBytes: 5242880,
|
|
10448
10786
|
maxPathBytes: 1024,
|
|
10449
10787
|
maxDepth: 64
|
|
@@ -10468,7 +10806,7 @@ function computeSnapshotDigest(entries) {
|
|
|
10468
10806
|
` : `${entry.path}\0${entry.sha256}\0${mode.toString(8)}
|
|
10469
10807
|
`;
|
|
10470
10808
|
}).join("");
|
|
10471
|
-
return
|
|
10809
|
+
return createHash7("sha256").update(Buffer.from(lines2, "utf8")).digest("hex");
|
|
10472
10810
|
}
|
|
10473
10811
|
function computeLegacySnapshotDigest(entries) {
|
|
10474
10812
|
return computeSnapshotDigest(
|
|
@@ -10685,6 +11023,53 @@ function isTooLarge(error) {
|
|
|
10685
11023
|
return error instanceof Error && error.message.includes("too large to read safely");
|
|
10686
11024
|
}
|
|
10687
11025
|
|
|
11026
|
+
// src/lib/instructions/staged-files.ts
|
|
11027
|
+
import { createHash as createHash8 } from "crypto";
|
|
11028
|
+
import { mkdtemp, realpath as realpath3, rm } from "fs/promises";
|
|
11029
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
11030
|
+
import path16 from "path";
|
|
11031
|
+
var IN_MEMORY_SNAPSHOT_BYTES = 50 * 1024 * 1024;
|
|
11032
|
+
async function stageFilesByUrl(options) {
|
|
11033
|
+
const root = await realpath3(
|
|
11034
|
+
await mkdtemp(path16.join(tmpdir2(), "fabric-instructions-"))
|
|
11035
|
+
);
|
|
11036
|
+
const files = /* @__PURE__ */ new Map();
|
|
11037
|
+
const dispose = () => rm(root, { recursive: true, force: true });
|
|
11038
|
+
try {
|
|
11039
|
+
await downloadFilesByUrl(options, async (file, bytes) => {
|
|
11040
|
+
const key = String(files.size);
|
|
11041
|
+
files.set(file.path, { ...file, key });
|
|
11042
|
+
await writeFileSafely({
|
|
11043
|
+
root,
|
|
11044
|
+
relativePath: key,
|
|
11045
|
+
bytes,
|
|
11046
|
+
mode: null
|
|
11047
|
+
});
|
|
11048
|
+
});
|
|
11049
|
+
} catch (error) {
|
|
11050
|
+
await dispose();
|
|
11051
|
+
throw error;
|
|
11052
|
+
}
|
|
11053
|
+
return {
|
|
11054
|
+
dispose,
|
|
11055
|
+
async get(filePath) {
|
|
11056
|
+
const file = files.get(filePath);
|
|
11057
|
+
if (!file) {
|
|
11058
|
+
return void 0;
|
|
11059
|
+
}
|
|
11060
|
+
const loaded = await readFileSafely(root, file.key, {
|
|
11061
|
+
maxBytes: file.size
|
|
11062
|
+
});
|
|
11063
|
+
if (!loaded || loaded.bytes.length !== file.size || createHash8("sha256").update(loaded.bytes).digest("hex") !== file.sha256) {
|
|
11064
|
+
throw new Error(
|
|
11065
|
+
`A staged file does not match the manifest (${filePath}).`
|
|
11066
|
+
);
|
|
11067
|
+
}
|
|
11068
|
+
return loaded.bytes;
|
|
11069
|
+
}
|
|
11070
|
+
};
|
|
11071
|
+
}
|
|
11072
|
+
|
|
10688
11073
|
// src/lib/instructions/doctor.ts
|
|
10689
11074
|
var MAX_ITEMS = 50;
|
|
10690
11075
|
var MAX_NAMES_IN_FIX = 20;
|
|
@@ -10812,7 +11197,7 @@ function buildFabricCommand(args, platform = process.platform) {
|
|
|
10812
11197
|
return launcher === null ? void 0 : [...launcher, ...words].join(" ");
|
|
10813
11198
|
}
|
|
10814
11199
|
function sha256Hex(bytes) {
|
|
10815
|
-
return
|
|
11200
|
+
return createHash9("sha256").update(bytes).digest("hex");
|
|
10816
11201
|
}
|
|
10817
11202
|
function listNames(names) {
|
|
10818
11203
|
const shown2 = names.slice(0, MAX_NAMES_IN_FIX);
|
|
@@ -12002,26 +12387,55 @@ async function downloadDeclaration(input, commands, client, initial, initialEntr
|
|
|
12002
12387
|
let manifest = initial.manifest;
|
|
12003
12388
|
let entry = initialEntry;
|
|
12004
12389
|
for (let attempt2 = 0; attempt2 < 2; attempt2++) {
|
|
12005
|
-
let download2;
|
|
12006
12390
|
try {
|
|
12007
|
-
|
|
12391
|
+
if (manifest.reduce((total, file) => total + file.size, 0) > IN_MEMORY_SNAPSHOT_BYTES) {
|
|
12392
|
+
const instructions = client().instructions;
|
|
12393
|
+
const sign = instructions.createFileDownloadUrls?.bind(instructions);
|
|
12394
|
+
if (!sign) {
|
|
12395
|
+
return {
|
|
12396
|
+
kind: "unreadable",
|
|
12397
|
+
status: "fail",
|
|
12398
|
+
detail: "this CLI cannot read a declaration from a large snapshot",
|
|
12399
|
+
fix: fixOf(
|
|
12400
|
+
"run the latest CLI from the project's Connect dialog"
|
|
12401
|
+
)
|
|
12402
|
+
};
|
|
12403
|
+
}
|
|
12404
|
+
const files = await fetchFilesByUrl({
|
|
12405
|
+
client: { instructions: { createFileDownloadUrls: sign } },
|
|
12406
|
+
projectId: input.projectId,
|
|
12407
|
+
org: input.org,
|
|
12408
|
+
digest: snapshot.digest,
|
|
12409
|
+
files: [entry],
|
|
12410
|
+
timeoutMs: 3e4,
|
|
12411
|
+
fetchImpl: input.fetchImpl
|
|
12412
|
+
});
|
|
12413
|
+
const bytes = files.get(entry.path);
|
|
12414
|
+
if (!bytes) {
|
|
12415
|
+
throw new Error("Published declaration missing");
|
|
12416
|
+
}
|
|
12417
|
+
return parseDeclarationBytes(bytes, "published");
|
|
12418
|
+
}
|
|
12419
|
+
const download2 = await input.createDownloadUrl(input.projectId, {
|
|
12008
12420
|
org: input.org
|
|
12009
12421
|
});
|
|
12422
|
+
if (download2.snapshotId === snapshot.id && download2.digest === snapshot.digest) {
|
|
12423
|
+
return fetchVerifiedDeclaration(
|
|
12424
|
+
input,
|
|
12425
|
+
download2.url,
|
|
12426
|
+
manifest,
|
|
12427
|
+
entry
|
|
12428
|
+
);
|
|
12429
|
+
}
|
|
12010
12430
|
} catch (error) {
|
|
12011
|
-
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
|
|
12015
|
-
|
|
12016
|
-
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
return fetchVerifiedDeclaration(
|
|
12020
|
-
input,
|
|
12021
|
-
download2.url,
|
|
12022
|
-
manifest,
|
|
12023
|
-
entry
|
|
12024
|
-
);
|
|
12431
|
+
if (!(error instanceof PublishedChangedError)) {
|
|
12432
|
+
return {
|
|
12433
|
+
kind: "unreadable",
|
|
12434
|
+
status: "fail",
|
|
12435
|
+
detail: `could not download the published declaration (${requestFailureClass(error)})`,
|
|
12436
|
+
fix: fixOf(NETWORK_FIX)
|
|
12437
|
+
};
|
|
12438
|
+
}
|
|
12025
12439
|
}
|
|
12026
12440
|
if (attempt2 > 0) {
|
|
12027
12441
|
break;
|
|
@@ -12449,13 +12863,13 @@ async function commandResolves(input, lookup, command) {
|
|
|
12449
12863
|
if (TOOL_NAME.test(command)) {
|
|
12450
12864
|
return await isOnPath(command, lookup) ? "found" : "missing";
|
|
12451
12865
|
}
|
|
12452
|
-
const api = input.platform === "win32" ?
|
|
12866
|
+
const api = input.platform === "win32" ? path17.win32 : path17.posix;
|
|
12453
12867
|
if (api.isAbsolute(command)) {
|
|
12454
12868
|
return await isExecutableFile(command, input.platform) ? "found" : "missing";
|
|
12455
12869
|
}
|
|
12456
12870
|
if (command.includes("/") || command.includes("\\")) {
|
|
12457
12871
|
return await isExecutableFile(
|
|
12458
|
-
|
|
12872
|
+
path17.resolve(input.root, command),
|
|
12459
12873
|
input.platform
|
|
12460
12874
|
) ? "found" : "missing";
|
|
12461
12875
|
}
|
|
@@ -12630,9 +13044,9 @@ function formatDoctorText(report, destination) {
|
|
|
12630
13044
|
}
|
|
12631
13045
|
|
|
12632
13046
|
// src/lib/instructions/drop-lock.ts
|
|
12633
|
-
import { createHash as
|
|
13047
|
+
import { createHash as createHash10 } from "crypto";
|
|
12634
13048
|
import { rmdir } from "fs/promises";
|
|
12635
|
-
import
|
|
13049
|
+
import path18 from "path";
|
|
12636
13050
|
var LOCK_RELATIVE = `${LOCK_DIRECTORY}/${LOCK_FILENAME}`;
|
|
12637
13051
|
var MAX_LOCK_BYTES2 = 16 * 1024 * 1024;
|
|
12638
13052
|
async function dropOwnLock(root, projectId) {
|
|
@@ -12647,12 +13061,12 @@ async function dropOwnLock(root, projectId) {
|
|
|
12647
13061
|
if (lock.projectId !== projectId) {
|
|
12648
13062
|
return false;
|
|
12649
13063
|
}
|
|
12650
|
-
const sha256 =
|
|
13064
|
+
const sha256 = createHash10("sha256").update(read.bytes).digest("hex");
|
|
12651
13065
|
const outcome = await deleteFileSafely(root, LOCK_RELATIVE, sha256);
|
|
12652
13066
|
if (outcome !== "deleted") {
|
|
12653
13067
|
return false;
|
|
12654
13068
|
}
|
|
12655
|
-
await rmdir(
|
|
13069
|
+
await rmdir(path18.join(root, LOCK_DIRECTORY)).catch(() => void 0);
|
|
12656
13070
|
return true;
|
|
12657
13071
|
} catch {
|
|
12658
13072
|
return false;
|
|
@@ -12744,8 +13158,8 @@ function replacePath(text3, target, replacement) {
|
|
|
12744
13158
|
}
|
|
12745
13159
|
|
|
12746
13160
|
// src/lib/instructions/fast-forward-run.ts
|
|
12747
|
-
import { mkdir as
|
|
12748
|
-
import
|
|
13161
|
+
import { mkdir as mkdir5, realpath as realpath4 } from "fs/promises";
|
|
13162
|
+
import path21 from "path";
|
|
12749
13163
|
|
|
12750
13164
|
// src/lib/instructions/fast-forward.ts
|
|
12751
13165
|
function fastForwardEligibility(facts) {
|
|
@@ -12916,6 +13330,14 @@ function fastForwardLines(outcome, context) {
|
|
|
12916
13330
|
stdout.push(outcomeLine("ff-locked", {}));
|
|
12917
13331
|
otherLine();
|
|
12918
13332
|
break;
|
|
13333
|
+
case "abandoned-lock":
|
|
13334
|
+
stdout.push(
|
|
13335
|
+
outcomeLine("ff-abandoned-lock", {
|
|
13336
|
+
lockPath: outcome.lockPath
|
|
13337
|
+
})
|
|
13338
|
+
);
|
|
13339
|
+
otherLine();
|
|
13340
|
+
break;
|
|
12919
13341
|
case "deadline":
|
|
12920
13342
|
stderr.push(
|
|
12921
13343
|
skipped(outcomeLine("gave-up", { after: context.gaveUpAfter }))
|
|
@@ -12929,10 +13351,10 @@ function fastForwardLines(outcome, context) {
|
|
|
12929
13351
|
}
|
|
12930
13352
|
|
|
12931
13353
|
// src/lib/instructions/fast-forward-memory.ts
|
|
12932
|
-
import { mkdir as
|
|
12933
|
-
import
|
|
13354
|
+
import { mkdir as mkdir3, readFile as readFile4, rename as rename2, rm as rm2, writeFile } from "fs/promises";
|
|
13355
|
+
import path19 from "path";
|
|
12934
13356
|
function noticeFile(commonDir2) {
|
|
12935
|
-
return
|
|
13357
|
+
return path19.join(commonDir2, "fabric", "ff-notice.json");
|
|
12936
13358
|
}
|
|
12937
13359
|
function isRecord4(value) {
|
|
12938
13360
|
if (typeof value !== "object" || value === null) {
|
|
@@ -12951,7 +13373,7 @@ async function isNewNotice(file, key) {
|
|
|
12951
13373
|
}
|
|
12952
13374
|
async function rememberNotice(file, key) {
|
|
12953
13375
|
try {
|
|
12954
|
-
await
|
|
13376
|
+
await mkdir3(path19.dirname(file), { recursive: true });
|
|
12955
13377
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
12956
13378
|
const record = { v: 1, ...key };
|
|
12957
13379
|
await writeFile(temporary, `${JSON.stringify(record)}
|
|
@@ -12963,7 +13385,7 @@ async function rememberNotice(file, key) {
|
|
|
12963
13385
|
}
|
|
12964
13386
|
}
|
|
12965
13387
|
async function forgetNotice(file) {
|
|
12966
|
-
await
|
|
13388
|
+
await rm2(file, { force: true }).catch(() => void 0);
|
|
12967
13389
|
}
|
|
12968
13390
|
|
|
12969
13391
|
// src/lib/instructions/hook-timing.ts
|
|
@@ -12975,11 +13397,11 @@ var hookTiming = {
|
|
|
12975
13397
|
};
|
|
12976
13398
|
|
|
12977
13399
|
// src/lib/instructions/hook-trace.ts
|
|
12978
|
-
import { mkdir as
|
|
12979
|
-
import
|
|
13400
|
+
import { mkdir as mkdir4, readFile as readFile5, rename as rename3, writeFile as writeFile2 } from "fs/promises";
|
|
13401
|
+
import path20 from "path";
|
|
12980
13402
|
var TRACE_LIMIT = 50;
|
|
12981
13403
|
function traceFile(configDir) {
|
|
12982
|
-
return
|
|
13404
|
+
return path20.join(configDir, "traces", "instructions-hook.jsonl");
|
|
12983
13405
|
}
|
|
12984
13406
|
async function appendTrace(file, entry) {
|
|
12985
13407
|
try {
|
|
@@ -12992,7 +13414,7 @@ async function appendTrace(file, entry) {
|
|
|
12992
13414
|
}
|
|
12993
13415
|
}).slice(-(TRACE_LIMIT - 1));
|
|
12994
13416
|
kept.push(JSON.stringify(entry));
|
|
12995
|
-
await
|
|
13417
|
+
await mkdir4(path20.dirname(file), { recursive: true, mode: 448 });
|
|
12996
13418
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
12997
13419
|
await writeFile2(temporary, `${kept.join("\n")}
|
|
12998
13420
|
`, { mode: 384 });
|
|
@@ -13006,7 +13428,7 @@ var MERGE_RESERVE_MS = 1500;
|
|
|
13006
13428
|
var FF_LOCK_STALE_MS = 3e4;
|
|
13007
13429
|
var show2 = (value, max = 200) => sanitizeDisplayText(value, max);
|
|
13008
13430
|
async function samePath(a, b) {
|
|
13009
|
-
const resolve = async (value) =>
|
|
13431
|
+
const resolve = async (value) => realpath4(value).catch(() => path21.resolve(value));
|
|
13010
13432
|
const [left, right] = await Promise.all([resolve(a), resolve(b)]);
|
|
13011
13433
|
return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
13012
13434
|
}
|
|
@@ -13110,6 +13532,10 @@ async function underLock(run3, first) {
|
|
|
13110
13532
|
return fetched;
|
|
13111
13533
|
}
|
|
13112
13534
|
const tip = fetched.tip;
|
|
13535
|
+
const afterFetch = await factsStillMatch(run3, facts, deadline);
|
|
13536
|
+
if (afterFetch !== null) {
|
|
13537
|
+
return afterFetch;
|
|
13538
|
+
}
|
|
13113
13539
|
if (tip === head) {
|
|
13114
13540
|
return { outcome: { kind: "already-current" }, head };
|
|
13115
13541
|
}
|
|
@@ -13124,6 +13550,13 @@ async function underLock(run3, first) {
|
|
|
13124
13550
|
if (deadline - Date.now() < MERGE_RESERVE_MS) {
|
|
13125
13551
|
return { outcome: { kind: "deadline" }, head };
|
|
13126
13552
|
}
|
|
13553
|
+
const beforeMerge = await factsStillMatch(run3, facts, deadline);
|
|
13554
|
+
if (beforeMerge !== null) {
|
|
13555
|
+
return beforeMerge;
|
|
13556
|
+
}
|
|
13557
|
+
if (deadline - Date.now() < MERGE_RESERVE_MS) {
|
|
13558
|
+
return { outcome: { kind: "deadline" }, head };
|
|
13559
|
+
}
|
|
13127
13560
|
const merged = await fastForwardTo(root, tip, deadline);
|
|
13128
13561
|
switch (merged.kind) {
|
|
13129
13562
|
case "merged":
|
|
@@ -13157,6 +13590,26 @@ async function underLock(run3, first) {
|
|
|
13157
13590
|
return merged;
|
|
13158
13591
|
}
|
|
13159
13592
|
}
|
|
13593
|
+
async function factsStillMatch(run3, expected, deadline) {
|
|
13594
|
+
const facts = await readFacts(run3, deadline);
|
|
13595
|
+
if (facts === null) {
|
|
13596
|
+
return unanswered(deadline, expected.state.head);
|
|
13597
|
+
}
|
|
13598
|
+
const gate = fastForwardEligibility(facts);
|
|
13599
|
+
if (!gate.eligible) {
|
|
13600
|
+
return {
|
|
13601
|
+
outcome: { kind: "not-safe", reason: gate.reason },
|
|
13602
|
+
head: facts.state.head
|
|
13603
|
+
};
|
|
13604
|
+
}
|
|
13605
|
+
if (facts.state.branch !== expected.state.branch || facts.state.head !== expected.state.head) {
|
|
13606
|
+
return {
|
|
13607
|
+
outcome: { kind: "not-safe", reason: "git-busy" },
|
|
13608
|
+
head: facts.state.head
|
|
13609
|
+
};
|
|
13610
|
+
}
|
|
13611
|
+
return null;
|
|
13612
|
+
}
|
|
13160
13613
|
async function decide(run3) {
|
|
13161
13614
|
const { classification, deadline, report } = run3;
|
|
13162
13615
|
const root = classification.toplevel;
|
|
@@ -13179,11 +13632,11 @@ async function decide(run3) {
|
|
|
13179
13632
|
if (common.kind !== "ok") {
|
|
13180
13633
|
return unanswered(deadline, head);
|
|
13181
13634
|
}
|
|
13182
|
-
const directory =
|
|
13635
|
+
const directory = path21.join(common.value, "fabric");
|
|
13183
13636
|
try {
|
|
13184
|
-
await
|
|
13637
|
+
await mkdir5(directory, { recursive: true });
|
|
13185
13638
|
return await withExclusiveLock(() => underLock(run3, facts), {
|
|
13186
|
-
lockPath:
|
|
13639
|
+
lockPath: path21.join(directory, "ff.lock"),
|
|
13187
13640
|
staleMs: FF_LOCK_STALE_MS,
|
|
13188
13641
|
waitMs: 0
|
|
13189
13642
|
});
|
|
@@ -13191,7 +13644,10 @@ async function decide(run3) {
|
|
|
13191
13644
|
if (error instanceof ExclusiveLockBusyError) {
|
|
13192
13645
|
const now = await headSha(root, deadline);
|
|
13193
13646
|
return {
|
|
13194
|
-
outcome:
|
|
13647
|
+
outcome: error.abandoned ? {
|
|
13648
|
+
kind: "abandoned-lock",
|
|
13649
|
+
lockPath: path21.join(directory, "ff.lock")
|
|
13650
|
+
} : { kind: "locked" },
|
|
13195
13651
|
head: now.kind === "ok" ? now.value : head
|
|
13196
13652
|
};
|
|
13197
13653
|
}
|
|
@@ -13309,99 +13765,11 @@ async function runFastForward(run3) {
|
|
|
13309
13765
|
return { outcome, stdout, stderr };
|
|
13310
13766
|
}
|
|
13311
13767
|
|
|
13312
|
-
// src/lib/instructions/file-downloads.ts
|
|
13313
|
-
import { createHash as createHash9 } from "crypto";
|
|
13314
|
-
var PER_FILE_MAX_WRITES = 100;
|
|
13315
|
-
var URL_REQUEST_CHUNK = 200;
|
|
13316
|
-
var DOWNLOAD_CONCURRENCY = 8;
|
|
13317
|
-
var PublishedChangedError = class extends Error {
|
|
13318
|
-
constructor() {
|
|
13319
|
-
super("The published version changed while this command ran.");
|
|
13320
|
-
this.name = "PublishedChangedError";
|
|
13321
|
-
}
|
|
13322
|
-
};
|
|
13323
|
-
async function fetchFilesByUrl(options) {
|
|
13324
|
-
const wanted = new Map(options.files.map((file) => [file.path, file]));
|
|
13325
|
-
const urls = /* @__PURE__ */ new Map();
|
|
13326
|
-
for (let i = 0; i < options.files.length; i += URL_REQUEST_CHUNK) {
|
|
13327
|
-
const paths = options.files.slice(i, i + URL_REQUEST_CHUNK).map((file) => file.path);
|
|
13328
|
-
let response;
|
|
13329
|
-
try {
|
|
13330
|
-
response = await options.client.instructions.createFileDownloadUrls(
|
|
13331
|
-
options.projectId,
|
|
13332
|
-
{ digest: options.digest, paths },
|
|
13333
|
-
{ org: options.org }
|
|
13334
|
-
);
|
|
13335
|
-
} catch (error) {
|
|
13336
|
-
if (error instanceof FabricError && error.status === 409 && error.code === "PUBLISHED_CHANGED") {
|
|
13337
|
-
throw new PublishedChangedError();
|
|
13338
|
-
}
|
|
13339
|
-
throw error;
|
|
13340
|
-
}
|
|
13341
|
-
if (response.digest !== options.digest) {
|
|
13342
|
-
throw new PublishedChangedError();
|
|
13343
|
-
}
|
|
13344
|
-
for (const file of response.files) {
|
|
13345
|
-
if (!wanted.has(file.path) || urls.has(file.path)) {
|
|
13346
|
-
throw new Error(
|
|
13347
|
-
"The server returned a file that was not asked for. Nothing was written."
|
|
13348
|
-
);
|
|
13349
|
-
}
|
|
13350
|
-
urls.set(file.path, file.url);
|
|
13351
|
-
}
|
|
13352
|
-
}
|
|
13353
|
-
if (urls.size !== wanted.size) {
|
|
13354
|
-
throw new Error(
|
|
13355
|
-
`The server returned ${urls.size} of ${wanted.size} requested files. Nothing was written.`
|
|
13356
|
-
);
|
|
13357
|
-
}
|
|
13358
|
-
const contents = /* @__PURE__ */ new Map();
|
|
13359
|
-
const queue = options.files.values();
|
|
13360
|
-
const failures = [];
|
|
13361
|
-
const worker = async () => {
|
|
13362
|
-
while (failures.length === 0) {
|
|
13363
|
-
const next = queue.next();
|
|
13364
|
-
if (next.done) {
|
|
13365
|
-
return;
|
|
13366
|
-
}
|
|
13367
|
-
const file = next.value;
|
|
13368
|
-
try {
|
|
13369
|
-
const bytes = await fetchBundle(urls.get(file.path) ?? "", {
|
|
13370
|
-
timeoutMs: options.timeoutMs,
|
|
13371
|
-
// The manifest's size, not the response's: the read is
|
|
13372
|
-
// abandoned the moment it passes what the plan expects.
|
|
13373
|
-
maxBytes: file.size,
|
|
13374
|
-
signal: options.signal,
|
|
13375
|
-
fetchImpl: options.fetchImpl
|
|
13376
|
-
});
|
|
13377
|
-
if (bytes.length !== file.size || createHash9("sha256").update(bytes).digest("hex") !== file.sha256) {
|
|
13378
|
-
throw new Error(
|
|
13379
|
-
`A downloaded file does not match the manifest (${file.path}). Nothing was written.`
|
|
13380
|
-
);
|
|
13381
|
-
}
|
|
13382
|
-
contents.set(file.path, bytes);
|
|
13383
|
-
} catch (error) {
|
|
13384
|
-
failures.push(error);
|
|
13385
|
-
}
|
|
13386
|
-
}
|
|
13387
|
-
};
|
|
13388
|
-
await Promise.all(
|
|
13389
|
-
Array.from(
|
|
13390
|
-
{ length: Math.min(DOWNLOAD_CONCURRENCY, options.files.length) },
|
|
13391
|
-
() => worker()
|
|
13392
|
-
)
|
|
13393
|
-
);
|
|
13394
|
-
if (failures.length > 0) {
|
|
13395
|
-
throw failures[0];
|
|
13396
|
-
}
|
|
13397
|
-
return contents;
|
|
13398
|
-
}
|
|
13399
|
-
|
|
13400
13768
|
// src/lib/instructions/git-exclude.ts
|
|
13401
|
-
import { appendFile, lstat as lstat6, mkdir as
|
|
13402
|
-
import
|
|
13769
|
+
import { appendFile, lstat as lstat6, mkdir as mkdir6, readFile as readFile6 } from "fs/promises";
|
|
13770
|
+
import path22 from "path";
|
|
13403
13771
|
function relativeToTopLevel(toplevel, directory, file) {
|
|
13404
|
-
return
|
|
13772
|
+
return path22.relative(toplevel, path22.join(directory, file)).split(path22.sep).join("/");
|
|
13405
13773
|
}
|
|
13406
13774
|
function isLiteralEntry(entry) {
|
|
13407
13775
|
return !/[\n\r*?[\]\\!]/.test(entry) && !entry.startsWith("#");
|
|
@@ -13435,7 +13803,7 @@ async function excludeLocalFiles(input) {
|
|
|
13435
13803
|
if (file.kind !== "ok") {
|
|
13436
13804
|
return { kind: "failed", entries: missing };
|
|
13437
13805
|
}
|
|
13438
|
-
await
|
|
13806
|
+
await mkdir6(path22.dirname(file.value), { recursive: true });
|
|
13439
13807
|
const existing = await lstat6(file.value).catch((error) => {
|
|
13440
13808
|
if (error.code === "ENOENT") {
|
|
13441
13809
|
return null;
|
|
@@ -13461,8 +13829,8 @@ async function excludeLocalFiles(input) {
|
|
|
13461
13829
|
}
|
|
13462
13830
|
|
|
13463
13831
|
// src/lib/instructions/hook-launcher.ts
|
|
13464
|
-
import { mkdir as
|
|
13465
|
-
import
|
|
13832
|
+
import { mkdir as mkdir7, readFile as readFile7, rename as rename4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
13833
|
+
import path23 from "path";
|
|
13466
13834
|
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.";
|
|
13467
13835
|
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.";
|
|
13468
13836
|
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.";
|
|
@@ -13470,7 +13838,7 @@ function deploymentFolder(origin) {
|
|
|
13470
13838
|
return origin.replace(/^(https?):\/\//, "$1-").replace(/[^A-Za-z0-9.-]+/g, "-");
|
|
13471
13839
|
}
|
|
13472
13840
|
function bundleCopyPath(configDirectory, origin) {
|
|
13473
|
-
return
|
|
13841
|
+
return path23.join(
|
|
13474
13842
|
configDirectory,
|
|
13475
13843
|
"cli",
|
|
13476
13844
|
deploymentFolder(origin),
|
|
@@ -13478,18 +13846,18 @@ function bundleCopyPath(configDirectory, origin) {
|
|
|
13478
13846
|
);
|
|
13479
13847
|
}
|
|
13480
13848
|
function samePath2(a, b) {
|
|
13481
|
-
const [left, right] = [
|
|
13849
|
+
const [left, right] = [path23.resolve(a), path23.resolve(b)];
|
|
13482
13850
|
return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
13483
13851
|
}
|
|
13484
13852
|
async function writeCopy(bytes, destination) {
|
|
13485
|
-
await
|
|
13853
|
+
await mkdir7(path23.dirname(destination), { recursive: true });
|
|
13486
13854
|
const temporary = `${destination}.${process.pid}.tmp`;
|
|
13487
13855
|
await writeFile3(temporary, bytes, { mode: 420 });
|
|
13488
13856
|
try {
|
|
13489
13857
|
await rename4(temporary, destination);
|
|
13490
13858
|
return true;
|
|
13491
13859
|
} catch (error) {
|
|
13492
|
-
await
|
|
13860
|
+
await rm3(temporary, { force: true });
|
|
13493
13861
|
const existing = await readFile7(destination).catch(() => null);
|
|
13494
13862
|
if (existing === null) {
|
|
13495
13863
|
throw error;
|
|
@@ -13564,10 +13932,10 @@ async function installHooks(input) {
|
|
|
13564
13932
|
}
|
|
13565
13933
|
|
|
13566
13934
|
// src/lib/instructions/lesson-prompt.ts
|
|
13567
|
-
import { createHash as
|
|
13935
|
+
import { createHash as createHash11 } from "crypto";
|
|
13568
13936
|
import { createReadStream } from "fs";
|
|
13569
|
-
import { mkdir as
|
|
13570
|
-
import
|
|
13937
|
+
import { mkdir as mkdir8, readdir as readdir2, stat as stat5, unlink as unlink2, writeFile as writeFile4 } from "fs/promises";
|
|
13938
|
+
import path24 from "path";
|
|
13571
13939
|
import { createInterface } from "readline";
|
|
13572
13940
|
function parseStopHookInput(text3) {
|
|
13573
13941
|
let parsed;
|
|
@@ -13649,7 +14017,7 @@ function countEditingToolUsesInLine(line2) {
|
|
|
13649
14017
|
return count;
|
|
13650
14018
|
}
|
|
13651
14019
|
function markerKey(sessionId, projectId) {
|
|
13652
|
-
return
|
|
14020
|
+
return createHash11("sha256").update(`${sessionId}:${projectId}`).digest("hex");
|
|
13653
14021
|
}
|
|
13654
14022
|
function lessonPromptReason(projectId) {
|
|
13655
14023
|
return `Fabric lesson check for project ${projectId}: before you finish, ask the developer once, in one short question: was there a mistake in this session the team should not repeat? If they name one, draft it as a lesson (a title, what happened, why it was a mistake, what to do instead, and the instruction files it relates to) and show them the draft. Only after they confirm, call the MCP tool fabric_add_instruction_lesson with projectId "${projectId}", title, body and relatedPaths; it opens a proposal a person approves, so report it as a suggestion awaiting review. If they say no, or the tool is not available, say so in one line and stop. Never call the tool without an explicit yes.`;
|
|
@@ -13658,14 +14026,14 @@ var MARKER_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
|
13658
14026
|
async function pruneOldMarkers(markerDir, now) {
|
|
13659
14027
|
let entries;
|
|
13660
14028
|
try {
|
|
13661
|
-
entries = await
|
|
14029
|
+
entries = await readdir2(markerDir);
|
|
13662
14030
|
} catch {
|
|
13663
14031
|
return;
|
|
13664
14032
|
}
|
|
13665
14033
|
const cutoff = now.getTime() - MARKER_MAX_AGE_MS;
|
|
13666
14034
|
await Promise.all(
|
|
13667
14035
|
entries.map(async (name) => {
|
|
13668
|
-
const entryPath =
|
|
14036
|
+
const entryPath = path24.join(markerDir, name);
|
|
13669
14037
|
try {
|
|
13670
14038
|
const stats = await stat5(entryPath);
|
|
13671
14039
|
if (stats.mtimeMs < cutoff) {
|
|
@@ -13716,7 +14084,7 @@ async function runLessonPromptInner(input) {
|
|
|
13716
14084
|
return { output: null, why: "stop-hook-active" };
|
|
13717
14085
|
}
|
|
13718
14086
|
try {
|
|
13719
|
-
await
|
|
14087
|
+
await mkdir8(input.markerDir, { recursive: true });
|
|
13720
14088
|
} catch (error) {
|
|
13721
14089
|
debugLog(`marker directory unavailable: ${describeError(error)}`);
|
|
13722
14090
|
return { output: null, why: "marker-unavailable" };
|
|
@@ -13725,7 +14093,7 @@ async function runLessonPromptInner(input) {
|
|
|
13725
14093
|
debugLog(`marker prune failed: ${describeError(error)}`);
|
|
13726
14094
|
});
|
|
13727
14095
|
const key = markerKey(parsedInput.sessionId, projectId);
|
|
13728
|
-
const markerPath =
|
|
14096
|
+
const markerPath = path24.join(input.markerDir, key);
|
|
13729
14097
|
if (await markerExists(markerPath)) {
|
|
13730
14098
|
return { output: null, why: "already-asked" };
|
|
13731
14099
|
}
|
|
@@ -13994,8 +14362,9 @@ function splitMessage(message) {
|
|
|
13994
14362
|
}
|
|
13995
14363
|
|
|
13996
14364
|
// src/lib/instructions/push.ts
|
|
13997
|
-
import { createHash as
|
|
14365
|
+
import { createHash as createHash12 } from "crypto";
|
|
13998
14366
|
var MAX_PUSH_CHANGES = 50;
|
|
14367
|
+
var MAX_INLINE_PUSH_BYTES = 2 * 1024 * 1024;
|
|
13999
14368
|
function assertPushablePath(candidate, source) {
|
|
14000
14369
|
const check = checkRelativePath(candidate);
|
|
14001
14370
|
if (!check.ok) {
|
|
@@ -14025,7 +14394,7 @@ function encodeContent(bytes) {
|
|
|
14025
14394
|
};
|
|
14026
14395
|
}
|
|
14027
14396
|
function sha256Of(bytes) {
|
|
14028
|
-
return
|
|
14397
|
+
return createHash12("sha256").update(bytes).digest("hex");
|
|
14029
14398
|
}
|
|
14030
14399
|
function refuseStaleLedger(detail) {
|
|
14031
14400
|
throw new Error(
|
|
@@ -14036,16 +14405,16 @@ function assertLockMatchesManifest(lock, manifest) {
|
|
|
14036
14405
|
const published = new Map(
|
|
14037
14406
|
manifest.map((entry) => [entry.path, entry.sha256])
|
|
14038
14407
|
);
|
|
14039
|
-
for (const [
|
|
14040
|
-
const expected = published.get(
|
|
14408
|
+
for (const [path29, entry] of Object.entries(lock.files)) {
|
|
14409
|
+
const expected = published.get(path29);
|
|
14041
14410
|
if (expected === void 0) {
|
|
14042
14411
|
refuseStaleLedger(
|
|
14043
|
-
`it lists ${
|
|
14412
|
+
`it lists ${path29}, which version ${lock.snapshotVersion} does not contain`
|
|
14044
14413
|
);
|
|
14045
14414
|
}
|
|
14046
14415
|
if (expected !== entry.sha256) {
|
|
14047
14416
|
refuseStaleLedger(
|
|
14048
|
-
`its record of ${
|
|
14417
|
+
`its record of ${path29} is not the published file`
|
|
14049
14418
|
);
|
|
14050
14419
|
}
|
|
14051
14420
|
}
|
|
@@ -14100,46 +14469,80 @@ async function computePushPlan(input) {
|
|
|
14100
14469
|
);
|
|
14101
14470
|
}
|
|
14102
14471
|
const entries = [];
|
|
14103
|
-
const changes = [];
|
|
14104
14472
|
const unchanged = [];
|
|
14105
14473
|
for (const lockedPath of lockedPaths.sort()) {
|
|
14106
|
-
const
|
|
14107
|
-
if (
|
|
14474
|
+
const fingerprint = await fingerprintFileSafely(root, lockedPath);
|
|
14475
|
+
if (fingerprint === null) {
|
|
14108
14476
|
entries.push({ path: lockedPath, action: "delete" });
|
|
14109
|
-
changes.push({ op: "delete", path: lockedPath });
|
|
14110
14477
|
continue;
|
|
14111
14478
|
}
|
|
14112
|
-
|
|
14113
|
-
if (actual === lock.files[lockedPath]?.sha256) {
|
|
14479
|
+
if (fingerprint.sha256 === lock.files[lockedPath]?.sha256) {
|
|
14114
14480
|
unchanged.push(lockedPath);
|
|
14115
14481
|
continue;
|
|
14116
14482
|
}
|
|
14117
|
-
const encoded = encodeContent(read.bytes);
|
|
14118
14483
|
entries.push({
|
|
14119
14484
|
path: lockedPath,
|
|
14120
14485
|
action: "put",
|
|
14121
|
-
size:
|
|
14122
|
-
sha256:
|
|
14486
|
+
size: fingerprint.size,
|
|
14487
|
+
sha256: fingerprint.sha256
|
|
14123
14488
|
});
|
|
14124
|
-
changes.push({ op: "put", path: lockedPath, ...encoded });
|
|
14125
14489
|
}
|
|
14126
14490
|
for (const newPath of added.sort()) {
|
|
14127
|
-
const
|
|
14128
|
-
if (
|
|
14491
|
+
const fingerprint = await fingerprintFileSafely(root, newPath);
|
|
14492
|
+
if (fingerprint === null) {
|
|
14129
14493
|
throw new Error(
|
|
14130
14494
|
`Refusing to push: there is no file at ${newPath}.`
|
|
14131
14495
|
);
|
|
14132
14496
|
}
|
|
14133
|
-
const encoded = encodeContent(read.bytes);
|
|
14134
14497
|
entries.push({
|
|
14135
14498
|
path: newPath,
|
|
14136
14499
|
action: "put",
|
|
14137
|
-
size:
|
|
14138
|
-
sha256:
|
|
14500
|
+
size: fingerprint.size,
|
|
14501
|
+
sha256: fingerprint.sha256
|
|
14502
|
+
});
|
|
14503
|
+
}
|
|
14504
|
+
return { entries, unchanged };
|
|
14505
|
+
}
|
|
14506
|
+
function pushContentBytes(entries) {
|
|
14507
|
+
return entries.reduce(
|
|
14508
|
+
(total, entry) => total + (entry.action === "put" ? entry.size : 0),
|
|
14509
|
+
0
|
|
14510
|
+
);
|
|
14511
|
+
}
|
|
14512
|
+
async function materializePushChanges(input) {
|
|
14513
|
+
if (input.entries.length > MAX_PUSH_CHANGES) {
|
|
14514
|
+
throw new Error(`Too many changes to push (${input.entries.length}).`);
|
|
14515
|
+
}
|
|
14516
|
+
if (pushContentBytes(input.entries) > MAX_INLINE_PUSH_BYTES) {
|
|
14517
|
+
throw new Error("The selected changes are too large to send inline.");
|
|
14518
|
+
}
|
|
14519
|
+
const changes = [];
|
|
14520
|
+
for (const entry of input.entries) {
|
|
14521
|
+
if (entry.action === "delete") {
|
|
14522
|
+
changes.push({ op: "delete", path: entry.path });
|
|
14523
|
+
continue;
|
|
14524
|
+
}
|
|
14525
|
+
const read = await readFileSafely(input.root, entry.path, {
|
|
14526
|
+
maxBytes: MAX_INLINE_PUSH_BYTES
|
|
14527
|
+
});
|
|
14528
|
+
if (read === null) {
|
|
14529
|
+
throw new Error(
|
|
14530
|
+
`Refusing to push: ${entry.path} disappeared after planning. Run \`${fabricCommand("instructions push")}\` again.`
|
|
14531
|
+
);
|
|
14532
|
+
}
|
|
14533
|
+
const sha256 = sha256Of(read.bytes);
|
|
14534
|
+
if (read.bytes.length !== entry.size || sha256 !== entry.sha256) {
|
|
14535
|
+
throw new Error(
|
|
14536
|
+
`Refusing to push: ${entry.path} changed after planning. Run \`${fabricCommand("instructions push")}\` again.`
|
|
14537
|
+
);
|
|
14538
|
+
}
|
|
14539
|
+
changes.push({
|
|
14540
|
+
op: "put",
|
|
14541
|
+
path: entry.path,
|
|
14542
|
+
...encodeContent(read.bytes)
|
|
14139
14543
|
});
|
|
14140
|
-
changes.push({ op: "put", path: newPath, ...encoded });
|
|
14141
14544
|
}
|
|
14142
|
-
return
|
|
14545
|
+
return changes;
|
|
14143
14546
|
}
|
|
14144
14547
|
var LIVE_PULL_REQUEST_STATES = /* @__PURE__ */ new Set([
|
|
14145
14548
|
"QUEUED",
|
|
@@ -14183,9 +14586,6 @@ function setAsideProposed(plan, proposals, baseSnapshotId) {
|
|
|
14183
14586
|
return {
|
|
14184
14587
|
plan: {
|
|
14185
14588
|
entries: plan.entries.filter((entry) => !setAside.has(entry.path)),
|
|
14186
|
-
changes: plan.changes.filter(
|
|
14187
|
-
(change) => !setAside.has(change.path)
|
|
14188
|
-
),
|
|
14189
14589
|
unchanged: plan.unchanged
|
|
14190
14590
|
},
|
|
14191
14591
|
alreadyProposed
|
|
@@ -14193,7 +14593,7 @@ function setAsideProposed(plan, proposals, baseSnapshotId) {
|
|
|
14193
14593
|
}
|
|
14194
14594
|
|
|
14195
14595
|
// src/lib/instructions/resolve-project.ts
|
|
14196
|
-
import
|
|
14596
|
+
import path25 from "path";
|
|
14197
14597
|
var MAX_CANDIDATES = 8;
|
|
14198
14598
|
var MAX_CANDIDATE_CHARS = 512;
|
|
14199
14599
|
var MAX_LABEL_CHARS = 80;
|
|
@@ -14262,7 +14662,7 @@ async function resolveProjectFromCheckout(input) {
|
|
|
14262
14662
|
for (const match of matches2) {
|
|
14263
14663
|
if (await sameDirectory(
|
|
14264
14664
|
destination,
|
|
14265
|
-
|
|
14665
|
+
path25.join(toplevel, match.rootPath)
|
|
14266
14666
|
)) {
|
|
14267
14667
|
here.push(match);
|
|
14268
14668
|
}
|
|
@@ -14285,9 +14685,9 @@ async function resolveProjectFromCheckout(input) {
|
|
|
14285
14685
|
}
|
|
14286
14686
|
|
|
14287
14687
|
// src/lib/instructions/self-update.ts
|
|
14288
|
-
import { createHash as
|
|
14289
|
-
import { mkdir as
|
|
14290
|
-
import
|
|
14688
|
+
import { createHash as createHash13 } from "crypto";
|
|
14689
|
+
import { mkdir as mkdir9, readFile as readFile8, realpath as realpath5, writeFile as writeFile5 } from "fs/promises";
|
|
14690
|
+
import path26 from "path";
|
|
14291
14691
|
|
|
14292
14692
|
// src/lib/instructions/npm-tarball.ts
|
|
14293
14693
|
import { gunzipSync } from "zlib";
|
|
@@ -14421,11 +14821,11 @@ function reasonToStayQuiet(input, now) {
|
|
|
14421
14821
|
return null;
|
|
14422
14822
|
}
|
|
14423
14823
|
async function isKeptCopy(script, copy) {
|
|
14424
|
-
const actual = await
|
|
14824
|
+
const actual = await realpath5(copy).catch(() => null);
|
|
14425
14825
|
if (actual === null) {
|
|
14426
14826
|
return false;
|
|
14427
14827
|
}
|
|
14428
|
-
const running = await
|
|
14828
|
+
const running = await realpath5(script).catch(() => path26.resolve(script));
|
|
14429
14829
|
return samePath2(running, actual);
|
|
14430
14830
|
}
|
|
14431
14831
|
async function readState2(file) {
|
|
@@ -14438,7 +14838,7 @@ async function readState2(file) {
|
|
|
14438
14838
|
}
|
|
14439
14839
|
async function writeState(file, state) {
|
|
14440
14840
|
try {
|
|
14441
|
-
await
|
|
14841
|
+
await mkdir9(path26.dirname(file), { recursive: true });
|
|
14442
14842
|
await writeFile5(file, `${JSON.stringify(state)}
|
|
14443
14843
|
`);
|
|
14444
14844
|
return true;
|
|
@@ -14557,7 +14957,7 @@ async function attempt({
|
|
|
14557
14957
|
signal,
|
|
14558
14958
|
fetchImpl
|
|
14559
14959
|
);
|
|
14560
|
-
const digest = `sha512-${
|
|
14960
|
+
const digest = `sha512-${createHash13("sha512").update(tarball).digest("base64")}`;
|
|
14561
14961
|
if (digest !== served.integrity) {
|
|
14562
14962
|
throw new SelfUpdateFailure(
|
|
14563
14963
|
"the download does not match the integrity hash the deployment published"
|
|
@@ -14588,7 +14988,7 @@ async function refreshKeptCopy(input, deps = {}) {
|
|
|
14588
14988
|
if (!await isKeptCopy(input.script, copy)) {
|
|
14589
14989
|
return skipped2("not-a-kept-copy");
|
|
14590
14990
|
}
|
|
14591
|
-
const stateFile =
|
|
14991
|
+
const stateFile = path26.join(path26.dirname(copy), STATE_FILE);
|
|
14592
14992
|
const previous = await readState2(stateFile);
|
|
14593
14993
|
if (previous !== null && askedRecently(previous, now())) {
|
|
14594
14994
|
return skipped2("checked-recently");
|
|
@@ -14646,7 +15046,7 @@ function selfUpdateLine(outcome) {
|
|
|
14646
15046
|
|
|
14647
15047
|
// src/lib/instructions/tools.ts
|
|
14648
15048
|
import { stat as stat6 } from "fs/promises";
|
|
14649
|
-
import
|
|
15049
|
+
import path27 from "path";
|
|
14650
15050
|
var TOOLS = [
|
|
14651
15051
|
{ tool: "claude-code", directory: ".claude", command: "claude" },
|
|
14652
15052
|
{ tool: "codex", directory: ".codex", command: "codex" }
|
|
@@ -14657,7 +15057,7 @@ async function isDirectory(target) {
|
|
|
14657
15057
|
async function detectTools(input) {
|
|
14658
15058
|
const present = [];
|
|
14659
15059
|
for (const facts of TOOLS) {
|
|
14660
|
-
const found = await isDirectory(
|
|
15060
|
+
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);
|
|
14661
15061
|
if (found) {
|
|
14662
15062
|
present.push(facts.tool);
|
|
14663
15063
|
}
|
|
@@ -14875,7 +15275,7 @@ async function refreshKeptCopyAfterHook(opts, deadlineAt) {
|
|
|
14875
15275
|
const outcome = await refreshKeptCopy({
|
|
14876
15276
|
origin,
|
|
14877
15277
|
script: bundleScriptPath(),
|
|
14878
|
-
configDirectory:
|
|
15278
|
+
configDirectory: path28.dirname(getConfigPath()),
|
|
14879
15279
|
runningTarball: bundleTarballPath(),
|
|
14880
15280
|
env: process.env,
|
|
14881
15281
|
deadlineAt,
|
|
@@ -14935,7 +15335,7 @@ function gitDeadline() {
|
|
|
14935
15335
|
return activeDeadlineAt === void 0 ? Date.now() + GIT_BUDGET_MS2 : activeDeadlineAt - hookTiming.gitMarginMs;
|
|
14936
15336
|
}
|
|
14937
15337
|
function destinationOf(opts) {
|
|
14938
|
-
return
|
|
15338
|
+
return path28.resolve(opts.dest ?? process.cwd());
|
|
14939
15339
|
}
|
|
14940
15340
|
function instructionsClient(opts, timeoutMs, { neverRetry = false } = {}) {
|
|
14941
15341
|
if (!getApiKey(deploymentOrigin(opts), opts.project)) {
|
|
@@ -15485,7 +15885,7 @@ async function syncOnce(opts) {
|
|
|
15485
15885
|
projectId: opts.project,
|
|
15486
15886
|
deadline: gitDeadline(),
|
|
15487
15887
|
optedOut: opts.fastForward === false,
|
|
15488
|
-
traceFile: traceFile(
|
|
15888
|
+
traceFile: traceFile(path28.dirname(getConfigPath()))
|
|
15489
15889
|
}) : null;
|
|
15490
15890
|
return {
|
|
15491
15891
|
kind: "reported",
|
|
@@ -15557,42 +15957,67 @@ async function syncOnce(opts) {
|
|
|
15557
15957
|
return { kind: "synced", outcome };
|
|
15558
15958
|
}
|
|
15559
15959
|
let contents = /* @__PURE__ */ new Map();
|
|
15560
|
-
|
|
15561
|
-
|
|
15562
|
-
|
|
15563
|
-
|
|
15564
|
-
manifest.map((entry) => [entry.path, entry])
|
|
15565
|
-
);
|
|
15960
|
+
let staged = null;
|
|
15961
|
+
let applied;
|
|
15962
|
+
try {
|
|
15963
|
+
for (let attempt2 = 0; plan.writes.length > 0 && attempt2 < 2; attempt2++) {
|
|
15566
15964
|
try {
|
|
15567
|
-
|
|
15568
|
-
|
|
15569
|
-
|
|
15570
|
-
|
|
15571
|
-
|
|
15572
|
-
|
|
15573
|
-
|
|
15574
|
-
|
|
15575
|
-
|
|
15576
|
-
|
|
15577
|
-
|
|
15578
|
-
|
|
15579
|
-
|
|
15965
|
+
const byPath3 = new Map(
|
|
15966
|
+
manifest.map((entry) => [entry.path, entry])
|
|
15967
|
+
);
|
|
15968
|
+
const large = manifest.reduce((total, entry) => total + entry.size, 0) > IN_MEMORY_SNAPSHOT_BYTES;
|
|
15969
|
+
if (large || attempt2 === 0 && plan.writes.length <= PER_FILE_MAX_WRITES) {
|
|
15970
|
+
const options = {
|
|
15971
|
+
client,
|
|
15972
|
+
projectId: opts.project,
|
|
15973
|
+
org: orgSlugFor(opts),
|
|
15974
|
+
digest: published.snapshot.digest,
|
|
15975
|
+
files: plan.writes.map((entry) => ({
|
|
15976
|
+
path: entry.path,
|
|
15977
|
+
size: byPath3.get(entry.path)?.size ?? -1,
|
|
15978
|
+
sha256: entry.sha256 ?? ""
|
|
15979
|
+
})),
|
|
15980
|
+
timeoutMs: opts.hook ? hookDeadlineMs() : BUNDLE_TIMEOUT_MS,
|
|
15981
|
+
signal: activeDeadline
|
|
15982
|
+
};
|
|
15983
|
+
if (large) {
|
|
15984
|
+
staged = await stageFilesByUrl(options);
|
|
15985
|
+
contents = staged;
|
|
15986
|
+
} else contents = await fetchFilesByUrl(options);
|
|
15987
|
+
} else {
|
|
15988
|
+
const download2 = await downloadUrlClient(
|
|
15989
|
+
opts
|
|
15990
|
+
).instructions.createDownloadUrl(opts.project, {
|
|
15991
|
+
org: orgSlugFor(opts)
|
|
15992
|
+
});
|
|
15993
|
+
const archive = await fetchBundle(download2.url, {
|
|
15994
|
+
timeoutMs: opts.hook ? hookDeadlineMs() : BUNDLE_TIMEOUT_MS,
|
|
15995
|
+
maxBytes: maxArchiveBytes(manifest),
|
|
15996
|
+
signal: activeDeadline
|
|
15997
|
+
});
|
|
15998
|
+
contents = extractBundle(
|
|
15999
|
+
archive,
|
|
16000
|
+
plan.writes.map((entry) => ({
|
|
16001
|
+
path: entry.path,
|
|
16002
|
+
size: byPath3.get(entry.path)?.size ?? -1
|
|
16003
|
+
}))
|
|
16004
|
+
);
|
|
16005
|
+
}
|
|
16006
|
+
break;
|
|
15580
16007
|
} catch (error) {
|
|
15581
|
-
if (!(error instanceof PublishedChangedError))
|
|
16008
|
+
if (!(error instanceof PublishedChangedError) || attempt2 > 0)
|
|
15582
16009
|
throw fixedFailure(error);
|
|
15583
|
-
}
|
|
15584
16010
|
published = await fetchPublished(
|
|
15585
16011
|
client,
|
|
15586
16012
|
opts,
|
|
15587
16013
|
void 0,
|
|
15588
16014
|
guard
|
|
15589
16015
|
);
|
|
15590
|
-
if (!published.published || !published.snapshot)
|
|
16016
|
+
if (!published.published || !published.snapshot)
|
|
15591
16017
|
throw new CliFailure(
|
|
15592
16018
|
"This project has no published coding instructions yet.",
|
|
15593
16019
|
4
|
|
15594
16020
|
);
|
|
15595
|
-
}
|
|
15596
16021
|
outcome.version = published.snapshot.version;
|
|
15597
16022
|
outcome.digest = published.snapshot.digest;
|
|
15598
16023
|
manifest = assertValidManifest({
|
|
@@ -15604,43 +16029,12 @@ async function syncOnce(opts) {
|
|
|
15604
16029
|
{ keepLocalEdits }
|
|
15605
16030
|
);
|
|
15606
16031
|
fillPlanPaths(outcome, plan);
|
|
15607
|
-
perFile = false;
|
|
15608
16032
|
}
|
|
15609
16033
|
}
|
|
15610
|
-
|
|
15611
|
-
|
|
15612
|
-
|
|
15613
|
-
try {
|
|
15614
|
-
download2 = await downloadUrlClient(
|
|
15615
|
-
opts
|
|
15616
|
-
).instructions.createDownloadUrl(opts.project, { org });
|
|
15617
|
-
} catch (error) {
|
|
15618
|
-
throw fixedFailure(error);
|
|
15619
|
-
}
|
|
15620
|
-
const sizes = new Map(
|
|
15621
|
-
manifest.map((entry) => [entry.path, entry.size])
|
|
15622
|
-
);
|
|
15623
|
-
const archive = await fetchBundle(download2.url, {
|
|
15624
|
-
timeoutMs: opts.hook ? hookDeadlineMs() : BUNDLE_TIMEOUT_MS,
|
|
15625
|
-
// Bounded by what this manifest says it holds, not by what the
|
|
15626
|
-
// response claims. The manifest has already been checked against
|
|
15627
|
-
// the published snapshot limits, so this is a number the client
|
|
15628
|
-
// decided.
|
|
15629
|
-
maxBytes: maxArchiveBytes(manifest),
|
|
15630
|
-
// In hook mode the same clock that bounds the command bounds the
|
|
15631
|
-
// download, rather than a second independent budget after it.
|
|
15632
|
-
signal: activeDeadline
|
|
15633
|
-
});
|
|
15634
|
-
contents = extractBundle(
|
|
15635
|
-
archive,
|
|
15636
|
-
plan.writes.map((entry) => ({
|
|
15637
|
-
path: entry.path,
|
|
15638
|
-
size: sizes.get(entry.path) ?? -1
|
|
15639
|
-
}))
|
|
15640
|
-
);
|
|
15641
|
-
}
|
|
16034
|
+
applied = await applyPlan({ root, plan, contents, keepLocalEdits });
|
|
16035
|
+
} finally {
|
|
16036
|
+
await staged?.dispose();
|
|
15642
16037
|
}
|
|
15643
|
-
const applied = await applyPlan({ root, plan, contents, keepLocalEdits });
|
|
15644
16038
|
outcome.removed = applied.deleted;
|
|
15645
16039
|
if (applied.keptEdited.length > 0) {
|
|
15646
16040
|
const late = new Set(applied.keptEdited);
|
|
@@ -15774,7 +16168,7 @@ async function runPush2(opts, format) {
|
|
|
15774
16168
|
manifest: published.manifest ?? [],
|
|
15775
16169
|
added: opts.add
|
|
15776
16170
|
});
|
|
15777
|
-
if (computed.
|
|
16171
|
+
if (computed.entries.length === 0) {
|
|
15778
16172
|
throw new CliFailure(
|
|
15779
16173
|
`Nothing to push: every file the last sync wrote still matches version ${lock.snapshotVersion}. Add a new file with --add <path> if you meant to suggest one.`,
|
|
15780
16174
|
7
|
|
@@ -15805,9 +16199,15 @@ async function runPush2(opts, format) {
|
|
|
15805
16199
|
);
|
|
15806
16200
|
}
|
|
15807
16201
|
}
|
|
15808
|
-
if (plan.
|
|
16202
|
+
if (plan.entries.length > MAX_PUSH_CHANGES) {
|
|
15809
16203
|
throw new CliFailure(
|
|
15810
|
-
`Too many changes to push (${plan.
|
|
16204
|
+
`Too many changes to push (${plan.entries.length} > ${MAX_PUSH_CHANGES}). A change set this large is a replacement rather than an edit \u2014 upload the folder from the project's Coding Instructions tab, which is also the only path that re-reads the project's exclusion rules.`,
|
|
16205
|
+
7
|
|
16206
|
+
);
|
|
16207
|
+
}
|
|
16208
|
+
if (pushContentBytes(plan.entries) > MAX_INLINE_PUSH_BYTES) {
|
|
16209
|
+
throw new CliFailure(
|
|
16210
|
+
`The selected changes are too large to send inline (over ${MAX_INLINE_PUSH_BYTES} bytes of file content). Upload the folder from the project's Coding Instructions tab instead.`,
|
|
15811
16211
|
7
|
|
15812
16212
|
);
|
|
15813
16213
|
}
|
|
@@ -15837,7 +16237,7 @@ async function runPush2(opts, format) {
|
|
|
15837
16237
|
published: null,
|
|
15838
16238
|
pullRequest: null
|
|
15839
16239
|
};
|
|
15840
|
-
if (plan.
|
|
16240
|
+
if (plan.entries.length === 0 && opts.publish) {
|
|
15841
16241
|
if (format === "json") {
|
|
15842
16242
|
printOutput(outcome, { format: "json" });
|
|
15843
16243
|
}
|
|
@@ -15852,17 +16252,26 @@ async function runPush2(opts, format) {
|
|
|
15852
16252
|
sawBranchWithoutPullRequest = true;
|
|
15853
16253
|
}
|
|
15854
16254
|
};
|
|
15855
|
-
if (!opts.dryRun && plan.
|
|
16255
|
+
if (!opts.dryRun && plan.entries.length > 0) {
|
|
16256
|
+
let changes;
|
|
16257
|
+
try {
|
|
16258
|
+
changes = await materializePushChanges({
|
|
16259
|
+
root,
|
|
16260
|
+
entries: plan.entries
|
|
16261
|
+
});
|
|
16262
|
+
} catch (error) {
|
|
16263
|
+
throw new CliFailure(describeError(error), 7);
|
|
16264
|
+
}
|
|
15856
16265
|
try {
|
|
15857
16266
|
const submitted = opts.publish ? await client.instructions.publishChange(
|
|
15858
16267
|
opts.project,
|
|
15859
16268
|
lock.snapshotId,
|
|
15860
|
-
|
|
16269
|
+
changes,
|
|
15861
16270
|
{ org: orgSlugFor(opts) }
|
|
15862
16271
|
) : await client.instructions.submitChange(
|
|
15863
16272
|
opts.project,
|
|
15864
16273
|
lock.snapshotId,
|
|
15865
|
-
|
|
16274
|
+
changes,
|
|
15866
16275
|
{ org: orgSlugFor(opts), ...note ? { note } : {} }
|
|
15867
16276
|
);
|
|
15868
16277
|
outcome.snapshotId = submitted.snapshotId;
|
|
@@ -16214,7 +16623,7 @@ function reportAlreadyProposed(outcome) {
|
|
|
16214
16623
|
var CLONE_TIMEOUT_MS = 5 * 6e4;
|
|
16215
16624
|
async function isEmptyFolder(folder) {
|
|
16216
16625
|
try {
|
|
16217
|
-
return (await
|
|
16626
|
+
return (await readdir3(folder)).length === 0;
|
|
16218
16627
|
} catch (error) {
|
|
16219
16628
|
return error.code === "ENOENT";
|
|
16220
16629
|
}
|
|
@@ -16278,12 +16687,14 @@ async function runInit(opts, format) {
|
|
|
16278
16687
|
destination = await resolveCloneFolder(destination, cloneFolder);
|
|
16279
16688
|
}
|
|
16280
16689
|
let existing = await resolveExistingRoot(destination);
|
|
16690
|
+
let cloned = false;
|
|
16691
|
+
const freshCloneDestination = cloneFolder !== void 0 && await isEmptyFolder(destination);
|
|
16281
16692
|
await assertKeyOutside(existing);
|
|
16282
16693
|
const client = instructionsClient(opts, SYNC_TIMEOUT_MS);
|
|
16283
16694
|
const guard = new SourceGuard(
|
|
16284
16695
|
destination,
|
|
16285
16696
|
opts.remote,
|
|
16286
|
-
|
|
16697
|
+
freshCloneDestination
|
|
16287
16698
|
);
|
|
16288
16699
|
const published = await fetchPublished(client, opts, void 0, guard);
|
|
16289
16700
|
const repository = published.repository ?? null;
|
|
@@ -16313,8 +16724,9 @@ async function runInit(opts, format) {
|
|
|
16313
16724
|
destination,
|
|
16314
16725
|
repository
|
|
16315
16726
|
);
|
|
16727
|
+
cloned = true;
|
|
16316
16728
|
if (cloneFolder !== void 0 && checkout.class === "unmapped") {
|
|
16317
|
-
destination =
|
|
16729
|
+
destination = path28.join(checkout.toplevel, checkout.rootPath);
|
|
16318
16730
|
existing = await resolveExistingRoot(destination);
|
|
16319
16731
|
checkout = await classifyCheckout({
|
|
16320
16732
|
destination: existing,
|
|
@@ -16356,6 +16768,13 @@ async function runInit(opts, format) {
|
|
|
16356
16768
|
}
|
|
16357
16769
|
}
|
|
16358
16770
|
const matching = checkout?.class === "matching" ? checkout : null;
|
|
16771
|
+
const interrupted = matching === null ? null : await checkoutAppearsIncomplete(
|
|
16772
|
+
matching.toplevel,
|
|
16773
|
+
gitDeadline()
|
|
16774
|
+
);
|
|
16775
|
+
if (interrupted?.kind === "ok" && interrupted.value) {
|
|
16776
|
+
throw outcomeFailure("clone-checkout-needs-repair", {});
|
|
16777
|
+
}
|
|
16359
16778
|
if (matching === null) {
|
|
16360
16779
|
await readLockForProject2(existing, opts.project);
|
|
16361
16780
|
}
|
|
@@ -16374,6 +16793,32 @@ async function runInit(opts, format) {
|
|
|
16374
16793
|
if (opts.lessons && !tools.tools.includes("claude-code")) {
|
|
16375
16794
|
throw outcomeFailure("lessons-need-claude", {});
|
|
16376
16795
|
}
|
|
16796
|
+
if (matching !== null) {
|
|
16797
|
+
const prefix = path28.relative(matching.toplevel, root);
|
|
16798
|
+
const relativeSetupPaths = [
|
|
16799
|
+
...tools.tools.map(
|
|
16800
|
+
(tool) => path28.join(prefix, hookPathFor(tool)).replaceAll(path28.sep, "/")
|
|
16801
|
+
),
|
|
16802
|
+
path28.join(prefix, LOCK_DIRECTORY, LOCK_FILENAME).replaceAll(path28.sep, "/")
|
|
16803
|
+
];
|
|
16804
|
+
const tracked = await trackedPaths(
|
|
16805
|
+
matching.toplevel,
|
|
16806
|
+
relativeSetupPaths,
|
|
16807
|
+
gitDeadline()
|
|
16808
|
+
);
|
|
16809
|
+
if (tracked.kind !== "ok") {
|
|
16810
|
+
throw new CliFailure(
|
|
16811
|
+
"Fabric could not verify whether its local setup files are tracked by Git, so it did not modify them. Check git status and run init again.",
|
|
16812
|
+
7
|
|
16813
|
+
);
|
|
16814
|
+
}
|
|
16815
|
+
if (tracked.value.length > 0) {
|
|
16816
|
+
throw new CliFailure(
|
|
16817
|
+
`Fabric left this checkout unchanged because Git tracks setup file${tracked.value.length === 1 ? "" : "s"}: ${tracked.value.join(", ")}. Keep ${tracked.value.length === 1 ? "it" : "them"} under the repository's normal setup process, or configure Fabric's hook and agent integration manually outside tracked paths.`,
|
|
16818
|
+
7
|
|
16819
|
+
);
|
|
16820
|
+
}
|
|
16821
|
+
}
|
|
16377
16822
|
let outcome = null;
|
|
16378
16823
|
if (checkout === null && published.published) {
|
|
16379
16824
|
const synced = await syncOnce({ ...opts, hook: false, guard });
|
|
@@ -16389,7 +16834,7 @@ async function runInit(opts, format) {
|
|
|
16389
16834
|
const applies = opts.reportOnly ? false : matching !== null ? true : Boolean(opts.apply);
|
|
16390
16835
|
const launcher = await resolveHookLauncher({
|
|
16391
16836
|
origin: deploymentOrigin(opts),
|
|
16392
|
-
configDirectory:
|
|
16837
|
+
configDirectory: path28.dirname(getConfigPath()),
|
|
16393
16838
|
lookup: { env: machine.env(), platform: machine.platform() },
|
|
16394
16839
|
bundle: isServedBundle() ? { scriptPath: bundleScriptPath() } : null
|
|
16395
16840
|
});
|
|
@@ -16427,7 +16872,8 @@ async function runInit(opts, format) {
|
|
|
16427
16872
|
deadline: gitDeadline()
|
|
16428
16873
|
});
|
|
16429
16874
|
const lockRemoved = matching === null ? false : await dropOwnLock(root, opts.project);
|
|
16430
|
-
const
|
|
16875
|
+
const mcpRequested = opts.mcp !== false && (tools.detected || explicitTool !== void 0);
|
|
16876
|
+
const mcp = !mcpRequested ? [] : await registerAgentMcp({
|
|
16431
16877
|
tools: tools.tools,
|
|
16432
16878
|
projectId: opts.project,
|
|
16433
16879
|
origin: deploymentOrigin(opts),
|
|
@@ -16443,6 +16889,8 @@ async function runInit(opts, format) {
|
|
|
16443
16889
|
}),
|
|
16444
16890
|
interactive: format !== "json" && canPrompt() && process.stdout.isTTY === true && !runningInCi()
|
|
16445
16891
|
});
|
|
16892
|
+
const mcpComplete = !mcpRequested || mcpRegistrationComplete(mcp);
|
|
16893
|
+
const mcpAuthPending = mcpAuthenticationPending(mcp);
|
|
16446
16894
|
const first = hooks[0];
|
|
16447
16895
|
if (format === "json") {
|
|
16448
16896
|
printOutput(
|
|
@@ -16461,6 +16909,9 @@ async function runInit(opts, format) {
|
|
|
16461
16909
|
replacedHooks: hook.replacedCount
|
|
16462
16910
|
})),
|
|
16463
16911
|
lessonsHook: Boolean(opts.lessons),
|
|
16912
|
+
mcpRequested,
|
|
16913
|
+
mcpComplete,
|
|
16914
|
+
mcpAuthenticationPending: mcpAuthPending,
|
|
16464
16915
|
mcp: mcp.map((result) => ({
|
|
16465
16916
|
tool: result.tool,
|
|
16466
16917
|
name: result.name,
|
|
@@ -16477,6 +16928,12 @@ async function runInit(opts, format) {
|
|
|
16477
16928
|
},
|
|
16478
16929
|
{ format: "json" }
|
|
16479
16930
|
);
|
|
16931
|
+
if (!mcpComplete) {
|
|
16932
|
+
throw new CliFailure(
|
|
16933
|
+
"Coding tool setup is incomplete: the Fabric MCP server was not registered for every selected tool. Complete the action shown above, then run init again.",
|
|
16934
|
+
7
|
|
16935
|
+
);
|
|
16936
|
+
}
|
|
16480
16937
|
return;
|
|
16481
16938
|
}
|
|
16482
16939
|
for (const hook of hooks) {
|
|
@@ -16512,12 +16969,17 @@ async function runInit(opts, format) {
|
|
|
16512
16969
|
for (const mcpLine of agentMcpLines(mcp)) {
|
|
16513
16970
|
line(mcpLine);
|
|
16514
16971
|
}
|
|
16515
|
-
if (
|
|
16972
|
+
if (mcpAuthPending) {
|
|
16973
|
+
line(
|
|
16974
|
+
"The Fabric MCP server is registered, but its coding-tool sign-in is still pending."
|
|
16975
|
+
);
|
|
16976
|
+
}
|
|
16977
|
+
if (cloneFolder !== void 0 && cloned) {
|
|
16516
16978
|
line(
|
|
16517
16979
|
outcomeLine("cloned-into", {
|
|
16518
16980
|
where: folderForCommand(
|
|
16519
16981
|
opts.dest,
|
|
16520
|
-
|
|
16982
|
+
path28.relative(
|
|
16521
16983
|
await resolveExistingRoot(destinationOf(opts)),
|
|
16522
16984
|
root
|
|
16523
16985
|
)
|
|
@@ -16525,6 +16987,15 @@ async function runInit(opts, format) {
|
|
|
16525
16987
|
})
|
|
16526
16988
|
);
|
|
16527
16989
|
}
|
|
16990
|
+
if (!mcpComplete) {
|
|
16991
|
+
line(
|
|
16992
|
+
"The session hook was set up, but the Fabric MCP server was not registered for every selected tool."
|
|
16993
|
+
);
|
|
16994
|
+
throw new CliFailure(
|
|
16995
|
+
"Coding tool setup is incomplete: the Fabric MCP server was not registered for every selected tool. Complete the action shown above, then run init again.",
|
|
16996
|
+
7
|
|
16997
|
+
);
|
|
16998
|
+
}
|
|
16528
16999
|
line(
|
|
16529
17000
|
outcomeLine("set-up", {
|
|
16530
17001
|
repo: matching !== null && repository !== null ? repositoryName(repository) : null,
|
|
@@ -16554,8 +17025,8 @@ async function runLessonPromptCommand(opts) {
|
|
|
16554
17025
|
stdinText = await readAllStdin(process.stdin);
|
|
16555
17026
|
} catch {
|
|
16556
17027
|
}
|
|
16557
|
-
const markerDir =
|
|
16558
|
-
|
|
17028
|
+
const markerDir = path28.join(
|
|
17029
|
+
path28.dirname(getConfigPath()),
|
|
16559
17030
|
"lesson-prompts"
|
|
16560
17031
|
);
|
|
16561
17032
|
const result = await runLessonPrompt({
|