@fabricorg/cli 0.5.1 → 0.6.0

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 CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/bin/fabric.ts
4
- import { Command as Command21 } from "commander";
4
+ import { Command as Command22 } from "commander";
5
5
 
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@fabricorg/cli",
9
- version: "0.5.1",
9
+ version: "0.6.0",
10
10
  description: "CLI for the Fabric AI platform",
11
11
  keywords: [
12
12
  "fabric",
@@ -38,7 +38,7 @@ var package_default = {
38
38
  build: "tsup",
39
39
  dev: "tsup --watch",
40
40
  "pack:deployment": "node scripts/pack-deployment.mjs",
41
- "type-check": "tsc --noEmit",
41
+ "type-check": "tsgo --noEmit",
42
42
  test: "vitest run",
43
43
  "test:watch": "vitest",
44
44
  fabric: "node --import tsx/esm src/bin/fabric.ts"
@@ -847,6 +847,32 @@ var InstructionsResource = class {
847
847
  `/projects/${encodeURIComponent(projectId)}/instructions/published${buildQuery3(options)}`
848
848
  );
849
849
  }
850
+ /**
851
+ * The credential-free metadata for a repository that Fabric reads directly.
852
+ * It resolves the configured branch once but never creates a snapshot,
853
+ * manifest, archive or provider write.
854
+ */
855
+ getRepositoryState(projectId, options = {}) {
856
+ return this.http.get(
857
+ `/projects/${encodeURIComponent(projectId)}/instructions/repository${buildQuery3(options)}`
858
+ );
859
+ }
860
+ /**
861
+ * Bounded repository metadata at one commit. The first call returns the
862
+ * commit pin; repeat calls must carry that pin so a moving branch cannot
863
+ * mix tree metadata with later file bytes.
864
+ */
865
+ listRepositoryFiles(projectId, options = {}) {
866
+ return this.http.get(
867
+ `/projects/${encodeURIComponent(projectId)}/instructions/repository/files${buildQuery3(options)}`
868
+ );
869
+ }
870
+ /** Read one text file from an already pinned direct repository commit. */
871
+ getRepositoryFile(projectId, options) {
872
+ return this.http.get(
873
+ `/projects/${encodeURIComponent(projectId)}/instructions/repository/file${buildQuery3(options)}`
874
+ );
875
+ }
850
876
  /**
851
877
  * Which projects use the repository a checkout was cloned from.
852
878
  *
@@ -947,6 +973,13 @@ var InstructionsResource = class {
947
973
  * against the five-per-proposer cap. The `snapshotId` a retry returns is
948
974
  * therefore the same `snapshotId`.
949
975
  */
976
+ submitRepositoryChange(projectId, nativeBase, changes, options = {}) {
977
+ const { note, ...context } = options;
978
+ return this.http.post(
979
+ `/projects/${encodeURIComponent(projectId)}/instructions/changes${buildQuery3(context)}`,
980
+ { nativeBase, changes, ...note ? { note } : {} }
981
+ );
982
+ }
950
983
  submitChange(projectId, baseSnapshotId, changes, options = {}) {
951
984
  const { note, ...context } = options;
952
985
  return this.http.post(
@@ -2368,12 +2401,13 @@ var CALLBACK_PATH = "/callback";
2368
2401
  var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
2369
2402
  var PAGE = (title, body) => `<!doctype html><meta charset="utf-8"><title>${title}</title><body style="font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem"><h1 style="font-weight:400">${title}</h1><p>${body}</p></body>`;
2370
2403
  async function startLoopbackListener(options) {
2404
+ const callbackPath = options.callbackPath ?? CALLBACK_PATH;
2371
2405
  let settle = () => {
2372
2406
  };
2373
2407
  const result = new Promise((resolve, reject) => {
2374
2408
  settle = (outcome) => {
2375
2409
  if ("code" in outcome) {
2376
- resolve({ code: outcome.code });
2410
+ resolve(outcome);
2377
2411
  } else {
2378
2412
  reject(outcome.error);
2379
2413
  }
@@ -2383,13 +2417,13 @@ async function startLoopbackListener(options) {
2383
2417
  });
2384
2418
  const server = createServer((request, response) => {
2385
2419
  const url = new URL(request.url ?? "/", "http://127.0.0.1");
2386
- if (request.method !== "GET" || url.pathname !== CALLBACK_PATH) {
2420
+ if (request.method !== "GET" || url.pathname !== callbackPath) {
2387
2421
  response.writeHead(404).end();
2388
2422
  return;
2389
2423
  }
2390
2424
  const error = url.searchParams.get("error");
2391
2425
  const state = url.searchParams.get("state");
2392
- const code = url.searchParams.get("code");
2426
+ const code = url.searchParams.get(options.codeParam ?? "code");
2393
2427
  if (state !== options.state) {
2394
2428
  response.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }).end(
2395
2429
  PAGE(
@@ -2428,12 +2462,20 @@ async function startLoopbackListener(options) {
2428
2462
  "You can close this tab and return to your terminal."
2429
2463
  )
2430
2464
  );
2431
- settle({ code });
2465
+ settle({ code, params: url.searchParams });
2432
2466
  });
2433
- await new Promise((resolve, reject) => {
2467
+ const listen = (port2) => new Promise((resolve, reject) => {
2434
2468
  server.once("error", reject);
2435
- server.listen(0, "127.0.0.1", () => resolve());
2469
+ server.listen(port2, "127.0.0.1", () => {
2470
+ server.off("error", reject);
2471
+ resolve();
2472
+ });
2436
2473
  });
2474
+ if (options.preferredPort === void 0) {
2475
+ await listen(0);
2476
+ } else {
2477
+ await listen(options.preferredPort).catch(() => listen(0));
2478
+ }
2437
2479
  const { port } = server.address();
2438
2480
  const timer = setTimeout(
2439
2481
  () => settle({
@@ -2451,7 +2493,7 @@ async function startLoopbackListener(options) {
2451
2493
  };
2452
2494
  result.then(close, close);
2453
2495
  return {
2454
- redirectUri: `http://127.0.0.1:${port}${CALLBACK_PATH}`,
2496
+ redirectUri: `http://127.0.0.1:${port}${callbackPath}`,
2455
2497
  result,
2456
2498
  close
2457
2499
  };
@@ -2501,6 +2543,7 @@ var REQUESTED_SCOPES = [
2501
2543
  "instructions:write",
2502
2544
  "offline_access"
2503
2545
  ];
2546
+ var REPOSITORY_TRANSPORT_SCOPE = "repositories:read";
2504
2547
  var CLIENT_NAME = "Fabric CLI";
2505
2548
  var UNUSABLE_CLIENT_ERRORS = /* @__PURE__ */ new Set([
2506
2549
  "invalid_client",
@@ -2542,7 +2585,7 @@ function forTerminal(text3) {
2542
2585
  }
2543
2586
  return printable2;
2544
2587
  }
2545
- async function registerClient(fetchImpl, metadata, redirectUri, signal) {
2588
+ async function registerClient(fetchImpl, metadata, redirectUri, signal, scopes) {
2546
2589
  const response = await fetchImpl(metadata.registration_endpoint, {
2547
2590
  method: "POST",
2548
2591
  headers: {
@@ -2555,8 +2598,8 @@ async function registerClient(fetchImpl, metadata, redirectUri, signal) {
2555
2598
  token_endpoint_auth_method: "none",
2556
2599
  grant_types: ["authorization_code", "refresh_token"],
2557
2600
  response_types: ["code"],
2558
- scope: REQUESTED_SCOPES.join(" "),
2559
- type: "native"
2601
+ scope: scopes.join(" "),
2602
+ application_type: "native"
2560
2603
  }),
2561
2604
  signal
2562
2605
  });
@@ -2634,6 +2677,7 @@ async function postTokenRequest(fetchImpl, endpoint, form, signal) {
2634
2677
  async function loginWithBrowser(options) {
2635
2678
  const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
2636
2679
  const now = options.now ?? Date.now;
2680
+ const scopes = options.scopes ?? REQUESTED_SCOPES;
2637
2681
  const metadata = await discoverAuthorizationServer(options.baseUrl, {
2638
2682
  fetch: fetchImpl,
2639
2683
  signal: options.signal
@@ -2651,7 +2695,7 @@ async function loginWithBrowser(options) {
2651
2695
  response_type: "code",
2652
2696
  client_id: clientId,
2653
2697
  redirect_uri: listener.redirectUri,
2654
- scope: REQUESTED_SCOPES.join(" "),
2698
+ scope: scopes.join(" "),
2655
2699
  state,
2656
2700
  code_challenge: codeChallengeS256(verifier),
2657
2701
  code_challenge_method: "S256",
@@ -2661,7 +2705,8 @@ async function loginWithBrowser(options) {
2661
2705
  };
2662
2706
  try {
2663
2707
  const previous = options.previous;
2664
- const reusable = previous !== void 0 && previous.tokenEndpoint === metadata.token_endpoint && sameLoopbackRedirect(previous.redirectUri, listener.redirectUri);
2708
+ const needsScopedRegistration = options.scopes !== void 0;
2709
+ const reusable = !needsScopedRegistration && previous !== void 0 && previous.tokenEndpoint === metadata.token_endpoint && sameLoopbackRedirect(previous.redirectUri, listener.redirectUri);
2665
2710
  let clientId;
2666
2711
  let registeredRedirectUri;
2667
2712
  if (reusable && !await clientIsUnusable(
@@ -2676,7 +2721,8 @@ async function loginWithBrowser(options) {
2676
2721
  fetchImpl,
2677
2722
  metadata,
2678
2723
  listener.redirectUri,
2679
- options.signal
2724
+ options.signal,
2725
+ scopes
2680
2726
  );
2681
2727
  registeredRedirectUri = listener.redirectUri;
2682
2728
  }
@@ -2844,6 +2890,13 @@ async function refreshAccessToken(needsRefresh, partial = {}, signal) {
2844
2890
  { lockPath: deps.lockPath, signal }
2845
2891
  );
2846
2892
  }
2893
+ async function projectAccessToken(origin, projectId, signal) {
2894
+ return refreshAccessToken(
2895
+ (current) => current.expiresAt - Date.now() <= REFRESH_WINDOW_MS,
2896
+ { origin, projectId },
2897
+ signal
2898
+ );
2899
+ }
2847
2900
  function createOAuthFetch(partial = {}, apiFetch = (input, init) => fetch(input, init)) {
2848
2901
  const deps = defaults(partial);
2849
2902
  const send = (input, init, token) => {
@@ -3583,8 +3636,9 @@ function decideCheckoutVerdict(input) {
3583
3636
 
3584
3637
  // src/lib/user-agent.ts
3585
3638
  var INSTRUCTIONS_STREAMING_CAPABILITY = "instructions-stream-v1";
3639
+ var INSTRUCTIONS_REPOSITORY_DIRECT_CAPABILITY = "instructions-repository-direct-v1";
3586
3640
  function cliUserAgent(version = package_default.version, nodeVersion = process.versions.node, platform = process.platform) {
3587
- return `fabric-cli/${version} (node/${nodeVersion}; ${platform}; ${INSTRUCTIONS_STREAMING_CAPABILITY})`;
3641
+ return `fabric-cli/${version} (node/${nodeVersion}; ${platform}; ${INSTRUCTIONS_STREAMING_CAPABILITY}; ${INSTRUCTIONS_REPOSITORY_DIRECT_CAPABILITY})`;
3588
3642
  }
3589
3643
  var MAX_NOTICE_CHARS = 300;
3590
3644
  var notice = null;
@@ -3954,7 +4008,8 @@ async function signInWithBrowser(input) {
3954
4008
  previous: getRegisteredClient(input.origin, input.project),
3955
4009
  openBrowser,
3956
4010
  announce: input.announce,
3957
- signal: input.signal
4011
+ signal: input.signal,
4012
+ ...input.scopes === void 0 ? {} : { scopes: input.scopes }
3958
4013
  });
3959
4014
  if (!isIssuedBy(credentials.issuer, input.origin)) {
3960
4015
  throw new SignInRefusedError(input.origin, "issued-elsewhere");
@@ -3978,6 +4033,10 @@ async function signInWithBrowser(input) {
3978
4033
  await revokeOAuthSession(credentials);
3979
4034
  throw new SignInRefusedError(input.origin, "rejected");
3980
4035
  }
4036
+ if (input.scopes?.some((scope) => !me.scopes.includes(scope))) {
4037
+ await revokeOAuthSession(credentials);
4038
+ throw new SignInRefusedError(input.origin, "rejected");
4039
+ }
3981
4040
  saveOAuth(credentials, {
3982
4041
  ...input.explicit ? { baseUrl: input.baseUrl } : { origin: input.origin },
3983
4042
  ...input.project === void 0 ? {} : { projectId: input.project }
@@ -4569,10 +4628,359 @@ function buildCompletionCommand() {
4569
4628
  return completion;
4570
4629
  }
4571
4630
 
4631
+ // src/commands/connect/chatgpt.ts
4632
+ import { randomUUID as randomUUID3 } from "crypto";
4633
+ import { Command as Command8 } from "commander";
4634
+
4635
+ // src/lib/chatgpt-plan/flow.ts
4636
+ import { randomBytes as randomBytes2, randomUUID as randomUUID2 } from "crypto";
4637
+ import { mkdir, readFile, writeFile } from "fs/promises";
4638
+ import { dirname, join } from "path";
4639
+ var CHATGPT_ISSUER = "https://auth.openai.com";
4640
+ var AUTHORIZE_URL = `${CHATGPT_ISSUER}/api/accounts/authorize`;
4641
+ var TOKEN_URL = `${CHATGPT_ISSUER}/api/accounts/oauth/token`;
4642
+ var RESOURCE = "https://api.openai.com/v1";
4643
+ var CHATGPT_PLAN_SCOPE = "chatgpt.tokens.use.direct";
4644
+ var SCOPES = `openid profile email offline_access resource.invoke ${CHATGPT_PLAN_SCOPE}`;
4645
+ var DYNAMIC_CLIENT_ID = "dynamic_agent_client";
4646
+ var CHATGPT_CALLBACK_PATH = "/auth/callback";
4647
+ var CHATGPT_CALLBACK_PORT = 1455;
4648
+ var AGENT_NAME = "Fabric";
4649
+ function registrationPath() {
4650
+ return join(dirname(getConfigPath()), "chatgpt-plan-host.json");
4651
+ }
4652
+ async function loadRegistration(path29 = registrationPath()) {
4653
+ try {
4654
+ return JSON.parse(await readFile(path29, "utf8"));
4655
+ } catch {
4656
+ return { hostId: `urn:uuid:${randomUUID2()}` };
4657
+ }
4658
+ }
4659
+ async function saveRegistration(registration, path29 = registrationPath()) {
4660
+ await mkdir(dirname(path29), { recursive: true, mode: 448 });
4661
+ await writeFile(path29, JSON.stringify(registration), { mode: 384 });
4662
+ }
4663
+ function randomToken() {
4664
+ return randomBytes2(24).toString("base64url");
4665
+ }
4666
+ function buildAuthorizeUrl(params) {
4667
+ const query = new URLSearchParams({
4668
+ client_id: params.clientId,
4669
+ ext_agent_host_id: params.hostId,
4670
+ response_type: "code",
4671
+ redirect_uri: params.redirectUri,
4672
+ scope: SCOPES,
4673
+ resource: RESOURCE,
4674
+ state: params.state,
4675
+ nonce: params.nonce,
4676
+ code_challenge: params.codeChallenge,
4677
+ code_challenge_method: "S256"
4678
+ });
4679
+ if (params.clientId === DYNAMIC_CLIENT_ID) {
4680
+ query.set("agent_name_hint", AGENT_NAME);
4681
+ }
4682
+ return `${AUTHORIZE_URL}?${query.toString()}`;
4683
+ }
4684
+ function issuedClientId(params, registration) {
4685
+ const issued = params.get("client_id") ?? registration.clientId;
4686
+ if (!issued || issued === DYNAMIC_CLIENT_ID) {
4687
+ throw new Error(
4688
+ "OpenAI did not issue a client id for this machine; run the command again"
4689
+ );
4690
+ }
4691
+ if (registration.clientId && issued !== registration.clientId) {
4692
+ throw new Error(
4693
+ "The sign-in came back for a different client than this machine's; run again with --new-registration"
4694
+ );
4695
+ }
4696
+ return issued;
4697
+ }
4698
+ async function exchangeCode(params, fetchImpl = fetch) {
4699
+ const response = await fetchImpl(TOKEN_URL, {
4700
+ method: "POST",
4701
+ headers: {
4702
+ "content-type": "application/x-www-form-urlencoded",
4703
+ accept: "application/json"
4704
+ },
4705
+ body: new URLSearchParams({
4706
+ grant_type: "authorization_code",
4707
+ client_id: params.clientId,
4708
+ code: params.code,
4709
+ code_verifier: params.codeVerifier,
4710
+ redirect_uri: params.redirectUri,
4711
+ resource: RESOURCE
4712
+ }).toString(),
4713
+ signal: AbortSignal.timeout(15e3)
4714
+ });
4715
+ const text3 = await response.text();
4716
+ if (!response.ok) {
4717
+ let code = `HTTP ${response.status}`;
4718
+ try {
4719
+ const parsed = JSON.parse(text3);
4720
+ if (typeof parsed.error === "string") {
4721
+ code = parsed.error;
4722
+ }
4723
+ } catch {
4724
+ }
4725
+ throw new Error(`OpenAI refused the sign-in code (${code})`);
4726
+ }
4727
+ return JSON.parse(text3);
4728
+ }
4729
+ function checkIdToken(idToken, expected) {
4730
+ const payload = idToken.split(".")[1];
4731
+ if (!payload) {
4732
+ throw new Error("OpenAI returned a malformed ID token");
4733
+ }
4734
+ const claims = JSON.parse(
4735
+ Buffer.from(payload, "base64url").toString("utf8")
4736
+ );
4737
+ if (claims.nonce !== expected.nonce) {
4738
+ throw new Error("The ID token does not belong to this sign-in");
4739
+ }
4740
+ if (!claims.sub) {
4741
+ throw new Error("The ID token names no account");
4742
+ }
4743
+ if (expected.subject && claims.sub !== expected.subject) {
4744
+ throw new Error(
4745
+ "Signed in as a different ChatGPT account than this machine's; run again with --new-registration to switch"
4746
+ );
4747
+ }
4748
+ return { sub: claims.sub, email: claims.email };
4749
+ }
4750
+ function buildUploadPayload(tokens, registration) {
4751
+ if (!tokens.refresh_token || !tokens.id_token) {
4752
+ throw new Error("OpenAI's reply is missing the refresh or ID token");
4753
+ }
4754
+ const scopes = (tokens.scope ?? "").split(" ").filter(Boolean);
4755
+ if (!scopes.includes(CHATGPT_PLAN_SCOPE)) {
4756
+ throw new Error(
4757
+ "ChatGPT plan usage was not allowed at sign-in; nothing was sent to Fabric"
4758
+ );
4759
+ }
4760
+ return {
4761
+ accessToken: tokens.access_token,
4762
+ refreshToken: tokens.refresh_token,
4763
+ idToken: tokens.id_token,
4764
+ tokenType: tokens.token_type,
4765
+ expiresIn: tokens.expires_in,
4766
+ scopes,
4767
+ clientId: registration.clientId,
4768
+ hostId: registration.hostId,
4769
+ ...tokens.earliest_refresh_at !== void 0 && {
4770
+ earliestRefreshAt: tokens.earliest_refresh_at
4771
+ }
4772
+ };
4773
+ }
4774
+ var FABRIC_APPROVAL_CALLBACK_PATH = "/fabric/callback";
4775
+ function buildApprovalUrl(origin, params) {
4776
+ const query = new URLSearchParams({
4777
+ port: String(params.port),
4778
+ state: params.state,
4779
+ ...params.sharedOrganizationSlug !== void 0 && {
4780
+ shared: "1",
4781
+ org: params.sharedOrganizationSlug
4782
+ }
4783
+ });
4784
+ return `${origin}/connect/chatgpt?${query.toString()}`;
4785
+ }
4786
+ async function uploadChatGptPlan(params, fetchImpl = fetch) {
4787
+ const response = await fetchImpl(
4788
+ `${params.origin}/api/connect/chatgpt/credentials`,
4789
+ {
4790
+ method: "POST",
4791
+ headers: {
4792
+ "content-type": "application/json",
4793
+ authorization: `Bearer ${params.ticket}`
4794
+ },
4795
+ body: JSON.stringify(params.payload),
4796
+ signal: AbortSignal.timeout(3e4)
4797
+ }
4798
+ );
4799
+ const body = await response.json().catch(() => ({}));
4800
+ if (!response.ok) {
4801
+ throw new Error(
4802
+ body.error ?? `Fabric refused the ChatGPT plan sign-in (HTTP ${response.status})`
4803
+ );
4804
+ }
4805
+ return body;
4806
+ }
4807
+
4808
+ // src/commands/connect/chatgpt.ts
4809
+ var CHATGPT_PLAN_NOT_ENABLED = "ChatGPT plan isn't enabled for any of your organizations. Ask your Fabric admin to enable it.";
4810
+ var CHATGPT_PLAN_SHARED_NOT_ALLOWED = "You can connect a shared ChatGPT plan account only to an organization you administer, with ChatGPT plan pooling enabled.";
4811
+ function connectOptionsError(opts) {
4812
+ if (opts.shared && !opts.org) {
4813
+ return "--shared needs --org <slug>: the organization the account is shared with.";
4814
+ }
4815
+ if (opts.org && !opts.shared) {
4816
+ return "--org is used only with --shared, to connect an account the organization shares.";
4817
+ }
4818
+ return null;
4819
+ }
4820
+ function open(url, what) {
4821
+ process.stdout.write(
4822
+ `Opening your browser to ${what}. If it does not open, visit:
4823
+ ${url}
4824
+ `
4825
+ );
4826
+ try {
4827
+ openBrowser(url);
4828
+ } catch {
4829
+ }
4830
+ }
4831
+ async function approveInFabric(origin, sharedOrganizationSlug) {
4832
+ const state = createState();
4833
+ const listener = await startLoopbackListener({
4834
+ state,
4835
+ callbackPath: FABRIC_APPROVAL_CALLBACK_PATH,
4836
+ codeParam: "ticket"
4837
+ });
4838
+ const port = Number(new URL(listener.redirectUri).port);
4839
+ open(
4840
+ buildApprovalUrl(origin, { port, state, sharedOrganizationSlug }),
4841
+ "approve the connection in Fabric"
4842
+ );
4843
+ try {
4844
+ return (await listener.result).code;
4845
+ } catch (err) {
4846
+ if (err.message.includes("chatgpt_plan_not_enabled")) {
4847
+ throw new Error(CHATGPT_PLAN_NOT_ENABLED);
4848
+ }
4849
+ if (err.message.includes("chatgpt_plan_shared_not_allowed")) {
4850
+ throw new Error(CHATGPT_PLAN_SHARED_NOT_ALLOWED);
4851
+ }
4852
+ throw err;
4853
+ } finally {
4854
+ listener.close();
4855
+ }
4856
+ }
4857
+ function describeOrganizations(result) {
4858
+ const enabled = result.organizations.filter((org) => org.enabled);
4859
+ if (enabled.length === 0) {
4860
+ return "Choose where to use it in Fabric settings \u2192 Account \u2192 AI providers.";
4861
+ }
4862
+ return `Your own AI work in ${enabled.map((org) => org.name).join(", ")} now uses your ChatGPT plan.`;
4863
+ }
4864
+ function describeSharedAccount(result) {
4865
+ const account = result.email ? ` (${result.email})` : "";
4866
+ const { organization, created } = result.shared;
4867
+ return `${created ? "Connected" : "Reconnected"} the ChatGPT account${account} as a shared account of ${organization.name}. Choose which work it serves in ${organization.name}'s settings \u2192 AI providers.`;
4868
+ }
4869
+ async function connect(opts) {
4870
+ const optionsError = connectOptionsError(opts);
4871
+ if (optionsError) {
4872
+ printError(optionsError, 2);
4873
+ }
4874
+ const origin = normalizeOrigin(
4875
+ opts.baseUrl ?? getBaseUrl() ?? DEFAULT_ORIGIN
4876
+ );
4877
+ if (origin === null) {
4878
+ printError(BAD_BASE_URL_LINE, 2);
4879
+ }
4880
+ const sharedOrganizationSlug = opts.shared ? opts.org : void 0;
4881
+ let ticket;
4882
+ try {
4883
+ ticket = await approveInFabric(origin, sharedOrganizationSlug);
4884
+ } catch (err) {
4885
+ printError(err.message, 1);
4886
+ }
4887
+ const stored = await loadRegistration();
4888
+ let registration = stored;
4889
+ if (sharedOrganizationSlug) {
4890
+ registration = { hostId: `urn:uuid:${randomUUID3()}` };
4891
+ } else if (opts.newRegistration) {
4892
+ registration = { hostId: stored.hostId };
4893
+ }
4894
+ const verifier = createCodeVerifier();
4895
+ const state = createState();
4896
+ const nonce = randomToken();
4897
+ const listener = await startLoopbackListener({
4898
+ state,
4899
+ callbackPath: CHATGPT_CALLBACK_PATH,
4900
+ preferredPort: CHATGPT_CALLBACK_PORT
4901
+ });
4902
+ open(
4903
+ buildAuthorizeUrl({
4904
+ clientId: registration.clientId ?? DYNAMIC_CLIENT_ID,
4905
+ hostId: registration.hostId,
4906
+ redirectUri: listener.redirectUri,
4907
+ state,
4908
+ nonce,
4909
+ codeChallenge: codeChallengeS256(verifier)
4910
+ }),
4911
+ "sign in with ChatGPT"
4912
+ );
4913
+ let result;
4914
+ let clientId;
4915
+ let subject;
4916
+ try {
4917
+ const { code, params } = await listener.result;
4918
+ clientId = issuedClientId(params, registration);
4919
+ if (!sharedOrganizationSlug) {
4920
+ await saveRegistration({ ...registration, clientId });
4921
+ }
4922
+ const tokens = await exchangeCode({
4923
+ clientId,
4924
+ code,
4925
+ codeVerifier: verifier,
4926
+ redirectUri: listener.redirectUri
4927
+ });
4928
+ subject = checkIdToken(tokens.id_token ?? "", {
4929
+ nonce,
4930
+ subject: registration.subject
4931
+ }).sub;
4932
+ result = await uploadChatGptPlan({
4933
+ origin,
4934
+ ticket,
4935
+ payload: buildUploadPayload(tokens, {
4936
+ clientId,
4937
+ hostId: registration.hostId
4938
+ })
4939
+ });
4940
+ } catch (err) {
4941
+ printError(err.message, 1);
4942
+ } finally {
4943
+ listener.close();
4944
+ }
4945
+ if ("shared" in result) {
4946
+ printSuccess(describeSharedAccount(result));
4947
+ return;
4948
+ }
4949
+ await saveRegistration({ hostId: registration.hostId, clientId, subject });
4950
+ printSuccess(
4951
+ `Connected your ChatGPT plan${result.email ? ` (${result.email})` : ""} to Fabric.`
4952
+ );
4953
+ process.stdout.write(`${describeOrganizations(result)}
4954
+ `);
4955
+ }
4956
+ function buildConnectCommand() {
4957
+ const connectCommand = new Command8("connect").description(
4958
+ "Connect a personal AI subscription to Fabric"
4959
+ );
4960
+ connectCommand.addCommand(
4961
+ new Command8("chatgpt").description(
4962
+ "Approve in Fabric, sign in with ChatGPT and run your own Fabric AI work on your plan"
4963
+ ).option(
4964
+ "--new-registration",
4965
+ "Register this machine with OpenAI again, for example to switch accounts"
4966
+ ).option(
4967
+ "--base-url <url>",
4968
+ "Deployment to connect, such as https://example.com"
4969
+ ).option(
4970
+ "--org <slug>",
4971
+ "With --shared: the organization to share the account with"
4972
+ ).option(
4973
+ "--shared",
4974
+ "Connect a ChatGPT account the organization shares, instead of your own plan (organization admins)"
4975
+ ).action(connect)
4976
+ );
4977
+ return connectCommand;
4978
+ }
4979
+
4572
4980
  // src/commands/context/index.ts
4573
4981
  import { lstat as lstat3 } from "fs/promises";
4574
4982
  import path6 from "path";
4575
- import { Command as Command8 } from "commander";
4983
+ import { Command as Command9 } from "commander";
4576
4984
 
4577
4985
  // src/lib/command-boundary.ts
4578
4986
  function outputFormatFor(command) {
@@ -4643,13 +5051,13 @@ function asCliFailure(error) {
4643
5051
  import path3 from "path";
4644
5052
 
4645
5053
  // src/lib/instructions/safe-write.ts
4646
- import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
5054
+ import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
4647
5055
  import { constants } from "fs";
4648
5056
  import {
4649
5057
  lstat,
4650
- mkdir,
4651
- open,
4652
- readFile,
5058
+ mkdir as mkdir2,
5059
+ open as open2,
5060
+ readFile as readFile2,
4653
5061
  realpath,
4654
5062
  rename,
4655
5063
  unlink
@@ -4810,7 +5218,7 @@ function permissionBits(mode) {
4810
5218
  return mode & 4095;
4811
5219
  }
4812
5220
  async function resolveDestinationRoot(destination) {
4813
- await mkdir(destination, { recursive: true });
5221
+ await mkdir2(destination, { recursive: true });
4814
5222
  return realpath(destination);
4815
5223
  }
4816
5224
  async function assertSafeTarget(root, relativePath) {
@@ -4875,7 +5283,7 @@ async function openFileSafely(root, relativePath) {
4875
5283
  }
4876
5284
  let handle;
4877
5285
  try {
4878
- handle = await open(
5286
+ handle = await open2(
4879
5287
  check.path,
4880
5288
  constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
4881
5289
  );
@@ -5002,7 +5410,7 @@ async function makeParents(root, relativePath) {
5002
5410
  current = path2.join(current, segment);
5003
5411
  const stats = await lstatOrNull(current);
5004
5412
  if (stats === null) {
5005
- await mkdir(current).catch(async (error) => {
5413
+ await mkdir2(current).catch(async (error) => {
5006
5414
  if (error.code !== "EEXIST") {
5007
5415
  throw error;
5008
5416
  }
@@ -5043,11 +5451,11 @@ async function writeFileSafely(input) {
5043
5451
  const directoryBefore = await lstatOrNull(directory);
5044
5452
  const temporary = path2.join(
5045
5453
  directory,
5046
- `.${path2.basename(target)}.${randomBytes2(8).toString("hex")}.tmp`
5454
+ `.${path2.basename(target)}.${randomBytes3(8).toString("hex")}.tmp`
5047
5455
  );
5048
5456
  let handle = null;
5049
5457
  try {
5050
- handle = await open(temporary, "wx", 384);
5458
+ handle = await open2(temporary, "wx", 384);
5051
5459
  await handle.writeFile(input.bytes);
5052
5460
  const bits = permissionBits(input.mode);
5053
5461
  if (bits !== null && process.platform !== "win32") {
@@ -5106,7 +5514,7 @@ async function deleteFileSafely(root, relativePath, expectedSha256) {
5106
5514
  }
5107
5515
  let actual;
5108
5516
  try {
5109
- actual = createHash2("sha256").update(await readFile(check.path)).digest("hex");
5517
+ actual = createHash2("sha256").update(await readFile2(check.path)).digest("hex");
5110
5518
  } catch (error) {
5111
5519
  if (error.code === "ENOENT") {
5112
5520
  return "missing";
@@ -6528,7 +6936,7 @@ function collectPattern(value, previous) {
6528
6936
  return [...previous, value];
6529
6937
  }
6530
6938
  function buildContextCommand() {
6531
- const context = new Command8("context").description(
6939
+ const context = new Command9("context").description(
6532
6940
  "Sync local knowledge files into a project's Context"
6533
6941
  );
6534
6942
  context.command("push").description(
@@ -6796,9 +7204,9 @@ function throwIfIncomplete(results, prune, pruneTargets) {
6796
7204
  }
6797
7205
 
6798
7206
  // src/commands/ctx/index.ts
6799
- import { Command as Command9 } from "commander";
7207
+ import { Command as Command10 } from "commander";
6800
7208
  function buildCtxCommand() {
6801
- const ctx = new Command9("ctx").description(
7209
+ const ctx = new Command10("ctx").description(
6802
7210
  "Manage the organization commands run in"
6803
7211
  );
6804
7212
  ctx.command("current").description("Show the active default context").action(() => {
@@ -6818,7 +7226,7 @@ function buildCtxCommand() {
6818
7226
  process.stdout.write(`Context: org:${stored.slug}
6819
7227
  `);
6820
7228
  });
6821
- const use = new Command9("use").description(
7229
+ const use = new Command10("use").description(
6822
7230
  "Set the default organization for commands"
6823
7231
  );
6824
7232
  use.command("personal").description("Retired \u2014 context is organization-only").action(() => {
@@ -6836,9 +7244,9 @@ function buildCtxCommand() {
6836
7244
  }
6837
7245
 
6838
7246
  // src/commands/features/index.ts
6839
- import { Command as Command10 } from "commander";
7247
+ import { Command as Command11 } from "commander";
6840
7248
  function buildFeaturesCommand() {
6841
- const features = new Command10("features").description(
7249
+ const features = new Command11("features").description(
6842
7250
  "Manage project features"
6843
7251
  );
6844
7252
  features.command("list").description("List features in a project").requiredOption("-p, --project <id>", "Project ID").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option(
@@ -7025,9 +7433,9 @@ function buildFeaturesCommand() {
7025
7433
  }
7026
7434
 
7027
7435
  // src/commands/frames/index.ts
7028
- import { Command as Command11 } from "commander";
7436
+ import { Command as Command12 } from "commander";
7029
7437
  function buildFramesCommand() {
7030
- const frames = new Command11("frames").description(
7438
+ const frames = new Command12("frames").description(
7031
7439
  "Manage AI-generated frames and slideshows"
7032
7440
  );
7033
7441
  frames.command("list").description("List frames and slideshows").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--limit <n>", "Max results (default 50)", "50").option(
@@ -7103,7 +7511,7 @@ function buildFramesCommand() {
7103
7511
  // src/commands/instructions/index.ts
7104
7512
  import { readdir as readdir3 } from "fs/promises";
7105
7513
  import path28 from "path";
7106
- import { Command as Command12 } from "commander";
7514
+ import { Command as Command13 } from "commander";
7107
7515
 
7108
7516
  // src/lib/environment.ts
7109
7517
  function isFlagSet(value) {
@@ -7171,7 +7579,7 @@ var STRIPPED_VARIABLES = /* @__PURE__ */ new Set([
7171
7579
  "GIT_CONFIG_SYSTEM",
7172
7580
  "GIT_CONFIG_NOSYSTEM"
7173
7581
  ]);
7174
- var STRIPPED_PATTERN = /^(?:FABRIC_|GIT_CONFIG_(?:KEY|VALUE)_\d+$)/;
7582
+ var STRIPPED_PATTERN = /^GIT_CONFIG_(?:KEY|VALUE)_\d+$/;
7175
7583
  var CREDENTIAL_PROGRAMS = /* @__PURE__ */ new Set(["GIT_ASKPASS", "SSH_ASKPASS"]);
7176
7584
  var SSH_PROGRAM_CHOICES = /* @__PURE__ */ new Set(["GIT_SSH_COMMAND", "GIT_SSH"]);
7177
7585
  function gitEnvironment(source = process.env, options = {}) {
@@ -7182,7 +7590,7 @@ function gitEnvironment(source = process.env, options = {}) {
7182
7590
  env2[name] = value;
7183
7591
  continue;
7184
7592
  }
7185
- if (STRIPPED_VARIABLES.has(upper) || STRIPPED_PATTERN.test(upper)) {
7593
+ if (STRIPPED_VARIABLES.has(upper) || upper.startsWith("FABRIC_") || upper.startsWith("GIT_TRACE") || STRIPPED_PATTERN.test(upper)) {
7186
7594
  continue;
7187
7595
  }
7188
7596
  env2[name] = value;
@@ -7204,6 +7612,34 @@ function gitEnvironment(source = process.env, options = {}) {
7204
7612
  function hookWriteEnvironment(source = process.env) {
7205
7613
  return gitEnvironment(source, { write: true, unattended: true });
7206
7614
  }
7615
+ function gatewayConfig(authorization) {
7616
+ if (authorization === void 0) {
7617
+ return { args: [], env: {} };
7618
+ }
7619
+ try {
7620
+ const url = new URL(authorization.url);
7621
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || authorization.authorization === "" || /[\r\n]/.test(authorization.authorization)) {
7622
+ return null;
7623
+ }
7624
+ const base = url.toString().replace(/\/$/, "");
7625
+ return {
7626
+ args: [
7627
+ "-c",
7628
+ `http.${base}.extraHeader=`,
7629
+ `--config-env=http.${base}.extraHeader=FABRIC_GIT_AUTH_HEADER`,
7630
+ "-c",
7631
+ "http.followRedirects=false",
7632
+ "-c",
7633
+ "credential.helper="
7634
+ ],
7635
+ env: {
7636
+ FABRIC_GIT_AUTH_HEADER: `Authorization: ${authorization.authorization}`
7637
+ }
7638
+ };
7639
+ } catch {
7640
+ return null;
7641
+ }
7642
+ }
7207
7643
  function runGit(cwd, args, deadline, options = {}) {
7208
7644
  const remaining = deadline - Date.now();
7209
7645
  if (remaining <= 0) {
@@ -7213,6 +7649,14 @@ function runGit(cwd, args, deadline, options = {}) {
7213
7649
  });
7214
7650
  }
7215
7651
  return new Promise((resolve) => {
7652
+ const gateway = gatewayConfig(options.httpAuthorization);
7653
+ if (gateway === null) {
7654
+ resolve({
7655
+ kind: "unavailable",
7656
+ reason: "invalid Fabric Git transport"
7657
+ });
7658
+ return;
7659
+ }
7216
7660
  let settled = false;
7217
7661
  const finish = (result) => {
7218
7662
  if (!settled) {
@@ -7226,6 +7670,7 @@ function runGit(cwd, args, deadline, options = {}) {
7226
7670
  child = spawn2(
7227
7671
  "git",
7228
7672
  [
7673
+ ...gateway.args,
7229
7674
  // Windows Git can report modified long-path files from its file
7230
7675
  // system cache even when Git's diff is empty. This command-local
7231
7676
  // setting keeps inspection truthful without changing the user's
@@ -7238,7 +7683,10 @@ function runGit(cwd, args, deadline, options = {}) {
7238
7683
  ],
7239
7684
  {
7240
7685
  cwd,
7241
- env: options.write && options.unattended ? hookWriteEnvironment(process.env) : gitEnvironment(process.env, options),
7686
+ env: {
7687
+ ...options.write && options.unattended ? hookWriteEnvironment(process.env) : gitEnvironment(process.env, options),
7688
+ ...gateway.env
7689
+ },
7242
7690
  stdio: ["ignore", "pipe", "pipe"],
7243
7691
  shell: false,
7244
7692
  windowsHide: true
@@ -7250,9 +7698,13 @@ function runGit(cwd, args, deadline, options = {}) {
7250
7698
  }
7251
7699
  const out = [];
7252
7700
  let outBytes = 0;
7701
+ let outOverflow = false;
7253
7702
  const err = [];
7254
7703
  let errBytes = 0;
7255
7704
  child.stdout?.on("data", (chunk) => {
7705
+ if (chunk.length > STDOUT_CAP_BYTES - outBytes) {
7706
+ outOverflow = true;
7707
+ }
7256
7708
  if (outBytes < STDOUT_CAP_BYTES) {
7257
7709
  const room = STDOUT_CAP_BYTES - outBytes;
7258
7710
  out.push(chunk.subarray(0, room));
@@ -7280,6 +7732,13 @@ function runGit(cwd, args, deadline, options = {}) {
7280
7732
  });
7281
7733
  child.on("close", (code) => {
7282
7734
  clearTimeout(escalate);
7735
+ if (outOverflow) {
7736
+ finish({
7737
+ kind: "unavailable",
7738
+ reason: "git output exceeded the supported size"
7739
+ });
7740
+ return;
7741
+ }
7283
7742
  finish({
7284
7743
  kind: "exited",
7285
7744
  code: code ?? -1,
@@ -7340,6 +7799,37 @@ async function simple(root, args, deadline) {
7340
7799
  };
7341
7800
  }
7342
7801
 
7802
+ // src/lib/instructions/git-diff.ts
7803
+ async function changedTrackedPaths(root, deadline) {
7804
+ const result = await simple(
7805
+ root,
7806
+ [
7807
+ "diff",
7808
+ "--name-only",
7809
+ "--no-renames",
7810
+ "--no-ext-diff",
7811
+ "--no-textconv",
7812
+ "--relative",
7813
+ "-z",
7814
+ "HEAD",
7815
+ "--",
7816
+ "."
7817
+ ],
7818
+ deadline
7819
+ );
7820
+ if (result.kind !== "ok") return result;
7821
+ if (result.value.code !== 0) {
7822
+ return {
7823
+ kind: "unavailable",
7824
+ reason: "Git could not compare this checkout to HEAD"
7825
+ };
7826
+ }
7827
+ return {
7828
+ kind: "ok",
7829
+ value: result.value.stdout.split("\0").filter(Boolean)
7830
+ };
7831
+ }
7832
+
7343
7833
  // src/lib/instructions/git-write.ts
7344
7834
  function fetchFailureOf(stderr) {
7345
7835
  if (/\(non-fast-forward\)|\[rejected\]|cannot fast-forward/i.test(stderr)) {
@@ -7371,6 +7861,9 @@ function mergeFailureOf(stderr) {
7371
7861
  )) {
7372
7862
  return "busy";
7373
7863
  }
7864
+ if (/would be overwritten by merge/i.test(stderr) && /local changes|untracked working tree files/i.test(stderr)) {
7865
+ return "local-changes";
7866
+ }
7374
7867
  if (/Not possible to fast-forward|refusing to merge unrelated histories|diverging branches/i.test(
7375
7868
  stderr
7376
7869
  )) {
@@ -7378,6 +7871,20 @@ function mergeFailureOf(stderr) {
7378
7871
  }
7379
7872
  return "other";
7380
7873
  }
7874
+ function blockedFilesOf(stderr) {
7875
+ const files = [];
7876
+ let listing = false;
7877
+ for (const line2 of stderr.split(/\r?\n/)) {
7878
+ if (/would be overwritten by merge:\s*$/i.test(line2)) {
7879
+ listing = true;
7880
+ } else if (listing && /^\s+\S/.test(line2)) {
7881
+ files.push(line2.trim());
7882
+ } else {
7883
+ listing = false;
7884
+ }
7885
+ }
7886
+ return files;
7887
+ }
7381
7888
  async function fetchRef(root, remote, ref, deadline) {
7382
7889
  if (!isRemoteName(remote) || !isBranchLiteral(ref)) {
7383
7890
  return { kind: "unavailable", reason: "not a branch this will fetch" };
@@ -7419,6 +7926,47 @@ async function fetchRef(root, remote, ref, deadline) {
7419
7926
  }
7420
7927
  return { kind: "fetched", tip: sha };
7421
7928
  }
7929
+ async function fetchRefFromUrl(root, url, remote, ref, deadline, httpAuthorization) {
7930
+ if (!isRemoteName(remote) || !isBranchLiteral(ref)) {
7931
+ return { kind: "unavailable", reason: "not a branch this will fetch" };
7932
+ }
7933
+ const tracking = `refs/remotes/${remote}/${ref}`;
7934
+ const fetched = await runGit(
7935
+ root,
7936
+ [
7937
+ "-c",
7938
+ "gc.auto=0",
7939
+ "-c",
7940
+ "maintenance.auto=false",
7941
+ "fetch",
7942
+ "--no-recurse-submodules",
7943
+ "--",
7944
+ url,
7945
+ `refs/heads/${ref}:${tracking}`
7946
+ ],
7947
+ deadline,
7948
+ { write: true, unattended: true, httpAuthorization }
7949
+ );
7950
+ if (fetched.kind === "unavailable") {
7951
+ return fetched.reason === "git timed out" ? { kind: "timed-out" } : { kind: "unavailable", reason: fetched.reason };
7952
+ }
7953
+ if (fetched.code !== 0) {
7954
+ return { kind: "failed", reason: fetchFailureOf(fetched.stderr) };
7955
+ }
7956
+ const tip = await runGit(
7957
+ root,
7958
+ ["rev-parse", "--verify", "-q", `${tracking}^{commit}`],
7959
+ deadline
7960
+ );
7961
+ if (tip.kind === "unavailable") {
7962
+ return tip.reason === "git timed out" ? { kind: "timed-out" } : { kind: "unavailable", reason: tip.reason };
7963
+ }
7964
+ const [sha] = lines(tip.stdout);
7965
+ if (tip.code !== 0 || !sha || !COMMIT_SHA.test(sha)) {
7966
+ return { kind: "failed", reason: "missing-ref" };
7967
+ }
7968
+ return { kind: "fetched", tip: sha };
7969
+ }
7422
7970
  async function fastForwardTo(root, sha, deadline) {
7423
7971
  if (!COMMIT_SHA.test(sha)) {
7424
7972
  return { kind: "unavailable", reason: "not a commit name" };
@@ -7433,7 +7981,8 @@ async function fastForwardTo(root, sha, deadline) {
7433
7981
  return merged.reason === "git timed out" ? { kind: "timed-out" } : { kind: "unavailable", reason: merged.reason };
7434
7982
  }
7435
7983
  if (merged.code !== 0) {
7436
- return { kind: "failed", reason: mergeFailureOf(merged.stderr) };
7984
+ const reason = mergeFailureOf(merged.stderr);
7985
+ return reason === "local-changes" ? { kind: "failed", reason, files: blockedFilesOf(merged.stderr) } : { kind: "failed", reason };
7437
7986
  }
7438
7987
  const head = await runGit(
7439
7988
  root,
@@ -7460,7 +8009,6 @@ var OPERATION_MARKERS = [
7460
8009
  // HEAD markers above name which one while a step is stopped.
7461
8010
  { marker: "sequencer", operation: "cherry-pick" }
7462
8011
  ];
7463
- var OWN_PATHS = [".claude/settings.local.json", ".codex/hooks.json"];
7464
8012
  async function nearestExistingDirectory(dir) {
7465
8013
  let current = path7.resolve(dir);
7466
8014
  for (; ; ) {
@@ -7627,28 +8175,26 @@ async function isAncestor(root, ancestorSha, descendantSha, deadline) {
7627
8175
  return { kind: "unavailable", reason: "the commit is not in this clone" };
7628
8176
  }
7629
8177
  async function isClean(root, deadline) {
7630
- const everythingElse = await porcelainIsEmpty(
7631
- root,
7632
- [
7633
- "--untracked-files=normal",
7634
- "--",
7635
- ":/",
7636
- ...OWN_PATHS.map((own) => `:(exclude,top)${own}`)
7637
- ],
7638
- deadline
7639
- );
7640
- if (everythingElse.kind !== "ok" || !everythingElse.value) {
7641
- return everythingElse;
7642
- }
7643
- return porcelainIsEmpty(
7644
- root,
7645
- [
7646
- "--untracked-files=no",
7647
- "--",
7648
- ...OWN_PATHS.map((own) => `:(top)${own}`)
7649
- ],
7650
- deadline
7651
- );
8178
+ for (const scope of [[], ["--cached"]]) {
8179
+ const result = await simple(
8180
+ root,
8181
+ ["diff", "--quiet", "--ignore-submodules=none", ...scope],
8182
+ deadline
8183
+ );
8184
+ if (result.kind !== "ok") {
8185
+ return result;
8186
+ }
8187
+ if (result.value.code === 1) {
8188
+ return { kind: "ok", value: false };
8189
+ }
8190
+ if (result.value.code !== 0) {
8191
+ return {
8192
+ kind: "unavailable",
8193
+ reason: `git exited with status ${result.value.code}`
8194
+ };
8195
+ }
8196
+ }
8197
+ return { kind: "ok", value: true };
7652
8198
  }
7653
8199
  async function trackedPaths(root, relativePaths, deadline) {
7654
8200
  if (relativePaths.length === 0) {
@@ -7721,23 +8267,6 @@ async function checkoutAppearsIncomplete(root, deadline) {
7721
8267
  value: headTree.value.stdout !== "" && staged.value.code === 1 && untracked.value.stdout !== ""
7722
8268
  };
7723
8269
  }
7724
- async function porcelainIsEmpty(root, args, deadline) {
7725
- const result = await simple(
7726
- root,
7727
- ["status", "--porcelain=v1", "--ignore-submodules=none", ...args],
7728
- deadline
7729
- );
7730
- if (result.kind !== "ok") {
7731
- return result;
7732
- }
7733
- if (result.value.code !== 0) {
7734
- return {
7735
- kind: "unavailable",
7736
- reason: `git exited with status ${result.value.code}`
7737
- };
7738
- }
7739
- return { kind: "ok", value: result.value.stdout.trim() === "" };
7740
- }
7741
8270
  async function operationInProgress(root, deadline) {
7742
8271
  const result = await simple(
7743
8272
  root,
@@ -7964,8 +8493,9 @@ function cloneFailureOf(stderr) {
7964
8493
  }
7965
8494
  return "other";
7966
8495
  }
7967
- async function cloneInto(dir, url, ref, deadline) {
7968
- if (cloneableUrl(url) !== url || !isBranchLiteral(ref)) {
8496
+ async function cloneInto(dir, url, ref, deadline, options = {}) {
8497
+ const validUrl = options.httpAuthorization ? options.httpAuthorization.url === url : cloneableUrl(url) === url;
8498
+ if (!validUrl || !isBranchLiteral(ref)) {
7969
8499
  return {
7970
8500
  kind: "unavailable",
7971
8501
  reason: "not a repository this will clone"
@@ -7990,13 +8520,28 @@ async function cloneInto(dir, url, ref, deadline) {
7990
8520
  "."
7991
8521
  ],
7992
8522
  deadline,
7993
- { write: true }
8523
+ { write: true, ...options }
7994
8524
  );
7995
8525
  if (result.kind === "unavailable") {
7996
8526
  return { kind: "unavailable", reason: result.reason };
7997
8527
  }
7998
8528
  return result.code === 0 ? { kind: "cloned" } : { kind: "failed", reason: cloneFailureOf(result.stderr) };
7999
8529
  }
8530
+ async function setRemoteUrl(root, remote, url, deadline) {
8531
+ if (!isRemoteName(remote) || cloneableUrl(url) !== url) {
8532
+ return { kind: "unavailable", reason: "not a repository remote" };
8533
+ }
8534
+ const result = await runGit(
8535
+ root,
8536
+ ["remote", "set-url", remote, url],
8537
+ deadline,
8538
+ { write: true }
8539
+ );
8540
+ if (result.kind === "unavailable") {
8541
+ return result;
8542
+ }
8543
+ return result.code === 0 ? { kind: "ok", value: void 0 } : { kind: "unavailable", reason: "git refused the provider remote" };
8544
+ }
8000
8545
 
8001
8546
  // src/lib/instructions/outcome.ts
8002
8547
  var HOOK_PREFIX = "fabric: coding instructions";
@@ -8005,6 +8550,15 @@ var TRAIT_TEXT = {
8005
8550
  sparse: " (sparse checkout)",
8006
8551
  superproject: " (inside a superproject)"
8007
8552
  };
8553
+ var MAX_BLOCKED_FILES = 3;
8554
+ function blockedFilesText(files) {
8555
+ if (files.length === 0) {
8556
+ return "";
8557
+ }
8558
+ const named = files.slice(0, MAX_BLOCKED_FILES).map(shown).join(", ");
8559
+ const more = files.length - MAX_BLOCKED_FILES;
8560
+ return ` (${named}${more > 0 ? ` and ${more} more` : ""})`;
8561
+ }
8008
8562
  function shown(value) {
8009
8563
  return sanitizeDisplayText(value, 255);
8010
8564
  }
@@ -8154,8 +8708,10 @@ var LINES = {
8154
8708
  return reason;
8155
8709
  }
8156
8710
  },
8157
- "ff-merge-failed": ({ reason, ref, remote, commands }) => {
8711
+ "ff-merge-failed": ({ reason, files, ref, remote, commands }) => {
8158
8712
  switch (reason) {
8713
+ case "local-changes":
8714
+ return `${HOOK_PREFIX}: ${ref} is behind ${remote}/${ref}, but local changes would be overwritten${blockedFilesText(files ?? [])}, so nothing was updated. Commit or stash them, then run: ${commands.pull}`;
8159
8715
  case "diverged":
8160
8716
  return `${HOOK_PREFIX}: ${ref} and ${remote}/${ref} have diverged, so nothing was updated. Run: ${commands.rebase}, or merge ${remote}/${ref} yourself.`;
8161
8717
  case "timeout":
@@ -8956,7 +9512,7 @@ function adoptionFor(input) {
8956
9512
  }
8957
9513
 
8958
9514
  // src/lib/instructions/agent-mcp-config.ts
8959
- import { readFile as readFile2, stat as stat2 } from "fs/promises";
9515
+ import { readFile as readFile3, stat as stat2 } from "fs/promises";
8960
9516
  import path10 from "path";
8961
9517
  var MAX_CONFIG_BYTES = 16 * 1024 * 1024;
8962
9518
  async function readText(file) {
@@ -8965,7 +9521,7 @@ async function readText(file) {
8965
9521
  if (!info.isFile() || info.size > MAX_CONFIG_BYTES) {
8966
9522
  return { kind: "unusable" };
8967
9523
  }
8968
- return { kind: "text", text: await readFile2(file, "utf8") };
9524
+ return { kind: "text", text: await readFile3(file, "utf8") };
8969
9525
  } catch (error) {
8970
9526
  return error.code === "ENOENT" ? { kind: "absent" } : { kind: "unusable" };
8971
9527
  }
@@ -9106,7 +9662,7 @@ async function readRegistration(input) {
9106
9662
 
9107
9663
  // src/lib/instructions/hook.ts
9108
9664
  import { createHash as createHash4 } from "crypto";
9109
- import { mkdir as mkdir2 } from "fs/promises";
9665
+ import { mkdir as mkdir3 } from "fs/promises";
9110
9666
  import { tmpdir } from "os";
9111
9667
  import path11 from "path";
9112
9668
 
@@ -9139,7 +9695,7 @@ function subcommandsForEvent(event) {
9139
9695
  }
9140
9696
  async function withHookConfigLock(root, posixPath, work) {
9141
9697
  const directory = path11.join(tmpdir(), "fabric-instructions-hook-locks");
9142
- await mkdir2(directory, { recursive: true });
9698
+ await mkdir3(directory, { recursive: true });
9143
9699
  const identity = createHash4("sha256").update(path11.resolve(root)).update("\0").update(posixPath).digest("hex");
9144
9700
  const lockPath2 = path11.join(directory, `${identity}.lock`);
9145
9701
  try {
@@ -10041,7 +10597,7 @@ import { createHash as createHash5 } from "crypto";
10041
10597
  import { chmod } from "fs/promises";
10042
10598
 
10043
10599
  // src/lib/instructions/lock.ts
10044
- import { readFile as readFile3 } from "fs/promises";
10600
+ import { readFile as readFile4 } from "fs/promises";
10045
10601
  import path14 from "path";
10046
10602
  var LOCK_DIRECTORY = ".fabric";
10047
10603
  var LOCK_FILENAME = "instructions.lock";
@@ -10056,7 +10612,7 @@ async function readLock(destination) {
10056
10612
  const file = lockPath(destination);
10057
10613
  let raw;
10058
10614
  try {
10059
- raw = await readFile3(file, "utf8");
10615
+ raw = await readFile4(file, "utf8");
10060
10616
  } catch (error) {
10061
10617
  if (error.code === "ENOENT") {
10062
10618
  return null;
@@ -13158,7 +13714,7 @@ function replacePath(text3, target, replacement) {
13158
13714
  }
13159
13715
 
13160
13716
  // src/lib/instructions/fast-forward-run.ts
13161
- import { mkdir as mkdir5, realpath as realpath4 } from "fs/promises";
13717
+ import { mkdir as mkdir6, realpath as realpath4 } from "fs/promises";
13162
13718
  import path21 from "path";
13163
13719
 
13164
13720
  // src/lib/instructions/fast-forward.ts
@@ -13192,9 +13748,6 @@ function fastForwardEligibility(facts) {
13192
13748
  if (facts.upstream !== `${remote}/${ref}`) {
13193
13749
  return no("upstream-mismatch");
13194
13750
  }
13195
- if (!state.clean) {
13196
- return no("dirty");
13197
- }
13198
13751
  if (facts.lockFiles) {
13199
13752
  return no("git-busy");
13200
13753
  }
@@ -13313,11 +13866,12 @@ function fastForwardLines(outcome, context) {
13313
13866
  case "merge-failed": {
13314
13867
  const text3 = outcomeLine("ff-merge-failed", {
13315
13868
  reason: outcome.reason,
13869
+ files: outcome.files,
13316
13870
  ref: context.ref,
13317
13871
  remote: context.remote,
13318
13872
  commands: context.commands
13319
13873
  });
13320
- if (outcome.reason === "diverged") {
13874
+ if (outcome.reason === "diverged" || outcome.reason === "local-changes") {
13321
13875
  stdout.push(text3);
13322
13876
  otherLine();
13323
13877
  } else {
@@ -13351,7 +13905,7 @@ function fastForwardLines(outcome, context) {
13351
13905
  }
13352
13906
 
13353
13907
  // src/lib/instructions/fast-forward-memory.ts
13354
- import { mkdir as mkdir3, readFile as readFile4, rename as rename2, rm as rm2, writeFile } from "fs/promises";
13908
+ import { mkdir as mkdir4, readFile as readFile5, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
13355
13909
  import path19 from "path";
13356
13910
  function noticeFile(commonDir2) {
13357
13911
  return path19.join(commonDir2, "fabric", "ff-notice.json");
@@ -13361,22 +13915,22 @@ function isRecord4(value) {
13361
13915
  return false;
13362
13916
  }
13363
13917
  const record = value;
13364
- return record.v === 1 && typeof record.projectId === "string" && typeof record.publishedVersion === "number" && typeof record.reason === "string";
13918
+ return record.v === 1 && typeof record.projectId === "string" && typeof record.publishedVersion === "number" && (record.directCommitSha === void 0 || typeof record.directCommitSha === "string") && typeof record.reason === "string";
13365
13919
  }
13366
13920
  async function isNewNotice(file, key) {
13367
13921
  try {
13368
- const parsed = JSON.parse(await readFile4(file, "utf8"));
13369
- return !(isRecord4(parsed) && parsed.projectId === key.projectId && parsed.publishedVersion === key.publishedVersion && parsed.reason === key.reason);
13922
+ const parsed = JSON.parse(await readFile5(file, "utf8"));
13923
+ return !(isRecord4(parsed) && parsed.projectId === key.projectId && parsed.publishedVersion === key.publishedVersion && parsed.directCommitSha === key.directCommitSha && parsed.reason === key.reason);
13370
13924
  } catch {
13371
13925
  return true;
13372
13926
  }
13373
13927
  }
13374
13928
  async function rememberNotice(file, key) {
13375
13929
  try {
13376
- await mkdir3(path19.dirname(file), { recursive: true });
13930
+ await mkdir4(path19.dirname(file), { recursive: true });
13377
13931
  const temporary = `${file}.${process.pid}.tmp`;
13378
13932
  const record = { v: 1, ...key };
13379
- await writeFile(temporary, `${JSON.stringify(record)}
13933
+ await writeFile2(temporary, `${JSON.stringify(record)}
13380
13934
  `, {
13381
13935
  mode: 384
13382
13936
  });
@@ -13397,7 +13951,7 @@ var hookTiming = {
13397
13951
  };
13398
13952
 
13399
13953
  // src/lib/instructions/hook-trace.ts
13400
- import { mkdir as mkdir4, readFile as readFile5, rename as rename3, writeFile as writeFile2 } from "fs/promises";
13954
+ import { mkdir as mkdir5, readFile as readFile6, rename as rename3, writeFile as writeFile3 } from "fs/promises";
13401
13955
  import path20 from "path";
13402
13956
  var TRACE_LIMIT = 50;
13403
13957
  function traceFile(configDir) {
@@ -13405,7 +13959,7 @@ function traceFile(configDir) {
13405
13959
  }
13406
13960
  async function appendTrace(file, entry) {
13407
13961
  try {
13408
- const previous = await readFile5(file, "utf8").catch(() => "");
13962
+ const previous = await readFile6(file, "utf8").catch(() => "");
13409
13963
  const kept = previous.split("\n").filter((line2) => {
13410
13964
  try {
13411
13965
  return typeof JSON.parse(line2)?.outcome === "string";
@@ -13414,9 +13968,9 @@ async function appendTrace(file, entry) {
13414
13968
  }
13415
13969
  }).slice(-(TRACE_LIMIT - 1));
13416
13970
  kept.push(JSON.stringify(entry));
13417
- await mkdir4(path20.dirname(file), { recursive: true, mode: 448 });
13971
+ await mkdir5(path20.dirname(file), { recursive: true, mode: 448 });
13418
13972
  const temporary = `${file}.${process.pid}.tmp`;
13419
- await writeFile2(temporary, `${kept.join("\n")}
13973
+ await writeFile3(temporary, `${kept.join("\n")}
13420
13974
  `, { mode: 384 });
13421
13975
  await rename3(temporary, file);
13422
13976
  } catch {
@@ -13425,6 +13979,7 @@ async function appendTrace(file, entry) {
13425
13979
 
13426
13980
  // src/lib/instructions/fast-forward-run.ts
13427
13981
  var MERGE_RESERVE_MS = 1500;
13982
+ var MERGE_FLOOR_MS = 500;
13428
13983
  var FF_LOCK_STALE_MS = 3e4;
13429
13984
  var show2 = (value, max = 200) => sanitizeDisplayText(value, max);
13430
13985
  async function samePath(a, b) {
@@ -13476,8 +14031,19 @@ async function readFacts(run3, deadline) {
13476
14031
  heldElsewhere
13477
14032
  };
13478
14033
  }
13479
- function unanswered(deadline, head) {
13480
- return Date.now() >= deadline ? { outcome: { kind: "deadline" }, head } : { outcome: { kind: "not-safe", reason: "git-busy" }, head };
14034
+ function unanswered(deadline, head, stage) {
14035
+ return Date.now() >= deadline ? { outcome: { kind: "deadline", stage }, head } : { outcome: { kind: "not-safe", reason: "git-busy" }, head };
14036
+ }
14037
+ async function tipRelation(root, head, tip, deadline) {
14038
+ if (tip === head) {
14039
+ return "current";
14040
+ }
14041
+ const forward = await isAncestor(root, head, tip, deadline);
14042
+ if (forward.kind !== "ok" || forward.value) {
14043
+ return "ahead";
14044
+ }
14045
+ const behind = await isAncestor(root, tip, head, deadline);
14046
+ return behind.kind === "ok" && behind.value ? "current" : "diverged";
13481
14047
  }
13482
14048
  async function underLock(run3, first) {
13483
14049
  const { classification, repository, deadline } = run3;
@@ -13486,7 +14052,7 @@ async function underLock(run3, first) {
13486
14052
  const ref = repository.ref;
13487
14053
  const facts = await readFacts(run3, deadline);
13488
14054
  if (facts === null) {
13489
- return unanswered(deadline, first.state.head);
14055
+ return unanswered(deadline, first.state.head, "read");
13490
14056
  }
13491
14057
  const gate = fastForwardEligibility(facts);
13492
14058
  if (!gate.eligible) {
@@ -13504,12 +14070,19 @@ async function underLock(run3, first) {
13504
14070
  }
13505
14071
  const fetchDeadline = deadline - MERGE_RESERVE_MS;
13506
14072
  if (Date.now() >= fetchDeadline) {
13507
- return { outcome: { kind: "deadline" }, head };
14073
+ return { outcome: { kind: "deadline", stage: "fetch" }, head };
13508
14074
  }
13509
- const fetched = await fetchRef(root, remote, ref, fetchDeadline);
14075
+ const fetched = run3.gitTransport ? await fetchRefFromUrl(
14076
+ root,
14077
+ run3.gitTransport.url,
14078
+ remote,
14079
+ ref,
14080
+ fetchDeadline,
14081
+ run3.gitTransport
14082
+ ) : await fetchRef(root, remote, ref, fetchDeadline);
13510
14083
  switch (fetched.kind) {
13511
14084
  case "timed-out":
13512
- return { outcome: { kind: "deadline" }, head };
14085
+ return { outcome: { kind: "deadline", stage: "fetch" }, head };
13513
14086
  case "unavailable":
13514
14087
  return {
13515
14088
  outcome: { kind: "fetch-failed", reason: "other" },
@@ -13532,30 +14105,26 @@ async function underLock(run3, first) {
13532
14105
  return fetched;
13533
14106
  }
13534
14107
  const tip = fetched.tip;
13535
- const afterFetch = await factsStillMatch(run3, facts, deadline);
13536
- if (afterFetch !== null) {
13537
- return afterFetch;
14108
+ const relation = await tipRelation(root, head, tip, deadline);
14109
+ const changed = await factsStillMatch(run3, facts, deadline);
14110
+ if (changed !== null) {
14111
+ return changed;
13538
14112
  }
13539
- if (tip === head) {
13540
- return { outcome: { kind: "already-current" }, head };
13541
- }
13542
- const forward = await isAncestor(root, head, tip, deadline);
13543
- if (forward.kind === "ok" && !forward.value) {
13544
- const behind = await isAncestor(root, tip, head, deadline);
13545
- return behind.kind === "ok" && behind.value ? { outcome: { kind: "already-current" }, head } : {
13546
- outcome: { kind: "merge-failed", reason: "diverged" },
13547
- head
13548
- };
13549
- }
13550
- if (deadline - Date.now() < MERGE_RESERVE_MS) {
13551
- return { outcome: { kind: "deadline" }, head };
13552
- }
13553
- const beforeMerge = await factsStillMatch(run3, facts, deadline);
13554
- if (beforeMerge !== null) {
13555
- return beforeMerge;
14113
+ switch (relation) {
14114
+ case "current":
14115
+ return { outcome: { kind: "already-current" }, head };
14116
+ case "diverged":
14117
+ return {
14118
+ outcome: { kind: "merge-failed", reason: "diverged" },
14119
+ head
14120
+ };
14121
+ case "ahead":
14122
+ break;
14123
+ default:
14124
+ return relation;
13556
14125
  }
13557
- if (deadline - Date.now() < MERGE_RESERVE_MS) {
13558
- return { outcome: { kind: "deadline" }, head };
14126
+ if (deadline - Date.now() < MERGE_FLOOR_MS) {
14127
+ return { outcome: { kind: "deadline", stage: "merge" }, head };
13559
14128
  }
13560
14129
  const merged = await fastForwardTo(root, tip, deadline);
13561
14130
  switch (merged.kind) {
@@ -13583,7 +14152,11 @@ async function underLock(run3, first) {
13583
14152
  };
13584
14153
  }
13585
14154
  return {
13586
- outcome: { kind: "merge-failed", reason: merged.reason },
14155
+ outcome: {
14156
+ kind: "merge-failed",
14157
+ reason: merged.reason,
14158
+ ...merged.files === void 0 ? {} : { files: merged.files }
14159
+ },
13587
14160
  head
13588
14161
  };
13589
14162
  default:
@@ -13593,7 +14166,7 @@ async function underLock(run3, first) {
13593
14166
  async function factsStillMatch(run3, expected, deadline) {
13594
14167
  const facts = await readFacts(run3, deadline);
13595
14168
  if (facts === null) {
13596
- return unanswered(deadline, expected.state.head);
14169
+ return unanswered(deadline, expected.state.head, "merge");
13597
14170
  }
13598
14171
  const gate = fastForwardEligibility(facts);
13599
14172
  if (!gate.eligible) {
@@ -13619,7 +14192,7 @@ async function decide(run3) {
13619
14192
  }
13620
14193
  const facts = await readFacts(run3, deadline);
13621
14194
  if (facts === null) {
13622
- return unanswered(deadline, head);
14195
+ return unanswered(deadline, head, "read");
13623
14196
  }
13624
14197
  const gate = fastForwardEligibility(facts);
13625
14198
  if (!gate.eligible) {
@@ -13630,11 +14203,11 @@ async function decide(run3) {
13630
14203
  }
13631
14204
  const common = await commonDir(root, deadline);
13632
14205
  if (common.kind !== "ok") {
13633
- return unanswered(deadline, head);
14206
+ return unanswered(deadline, head, "read");
13634
14207
  }
13635
14208
  const directory = path21.join(common.value, "fabric");
13636
14209
  try {
13637
- await mkdir5(directory, { recursive: true });
14210
+ await mkdir6(directory, { recursive: true });
13638
14211
  return await withExclusiveLock(() => underLock(run3, facts), {
13639
14212
  lockPath: path21.join(directory, "ff.lock"),
13640
14213
  staleMs: FF_LOCK_STALE_MS,
@@ -13660,6 +14233,8 @@ function reasonOf(outcome) {
13660
14233
  case "fetch-failed":
13661
14234
  case "merge-failed":
13662
14235
  return outcome.reason;
14236
+ case "deadline":
14237
+ return outcome.stage;
13663
14238
  default:
13664
14239
  return null;
13665
14240
  }
@@ -13668,8 +14243,8 @@ function noticeReason(outcome, lag) {
13668
14243
  if (outcome.kind === "not-safe") {
13669
14244
  return `not-safe:${outcome.reason}`;
13670
14245
  }
13671
- if (outcome.kind === "merge-failed" && outcome.reason === "diverged") {
13672
- return "merge-failed:diverged";
14246
+ if (outcome.kind === "merge-failed" && (outcome.reason === "diverged" || outcome.reason === "local-changes")) {
14247
+ return `merge-failed:${outcome.reason}`;
13673
14248
  }
13674
14249
  if ((outcome.kind === "fast-forwarded" || outcome.kind === "already-current") && lag !== null) {
13675
14250
  return `fabric-lags:${lag}`;
@@ -13694,7 +14269,8 @@ async function runFastForward(run3) {
13694
14269
  const decision = await decide(run3);
13695
14270
  const { outcome } = decision;
13696
14271
  const source = snapshot?.source;
13697
- const publishedSha = source?.kind === "REPOSITORY" && source.current && isCommitSha(source.commitSha) ? source.commitSha : null;
14272
+ const snapshotSha = source?.kind === "REPOSITORY" && source.current && isCommitSha(source.commitSha) ? source.commitSha : null;
14273
+ const publishedSha = run3.directCommitSha ?? snapshotSha;
13698
14274
  const moved = outcome.kind === "fast-forwarded" || outcome.kind === "already-current";
13699
14275
  const head = outcome.kind === "fast-forwarded" ? outcome.to : decision.head;
13700
14276
  let publishedInHistory = null;
@@ -13707,7 +14283,7 @@ async function runFastForward(run3) {
13707
14283
  );
13708
14284
  publishedInHistory = answer.kind === "ok" ? answer.value : null;
13709
14285
  }
13710
- const lag = moved ? fabricCopyLag({
14286
+ const lag = moved && snapshot !== void 0 ? fabricCopyLag({
13711
14287
  headSha: head,
13712
14288
  publishedSha,
13713
14289
  publishedInHistory,
@@ -13744,6 +14320,7 @@ async function runFastForward(run3) {
13744
14320
  const key = {
13745
14321
  projectId: run3.projectId,
13746
14322
  publishedVersion: snapshot ? Number(snapshot.version) : 0,
14323
+ ...run3.directCommitSha === void 0 ? {} : { directCommitSha: run3.directCommitSha },
13747
14324
  reason
13748
14325
  };
13749
14326
  if (await isNewNotice(notices, key)) {
@@ -13766,7 +14343,7 @@ async function runFastForward(run3) {
13766
14343
  }
13767
14344
 
13768
14345
  // src/lib/instructions/git-exclude.ts
13769
- import { appendFile, lstat as lstat6, mkdir as mkdir6, readFile as readFile6 } from "fs/promises";
14346
+ import { appendFile, lstat as lstat6, mkdir as mkdir7, readFile as readFile7 } from "fs/promises";
13770
14347
  import path22 from "path";
13771
14348
  function relativeToTopLevel(toplevel, directory, file) {
13772
14349
  return path22.relative(toplevel, path22.join(directory, file)).split(path22.sep).join("/");
@@ -13803,7 +14380,7 @@ async function excludeLocalFiles(input) {
13803
14380
  if (file.kind !== "ok") {
13804
14381
  return { kind: "failed", entries: missing };
13805
14382
  }
13806
- await mkdir6(path22.dirname(file.value), { recursive: true });
14383
+ await mkdir7(path22.dirname(file.value), { recursive: true });
13807
14384
  const existing = await lstat6(file.value).catch((error) => {
13808
14385
  if (error.code === "ENOENT") {
13809
14386
  return null;
@@ -13813,7 +14390,7 @@ async function excludeLocalFiles(input) {
13813
14390
  if (existing !== null && !existing.isFile()) {
13814
14391
  return { kind: "failed", entries: missing };
13815
14392
  }
13816
- const text3 = existing === null ? "" : await readFile6(file.value, "utf8");
14393
+ const text3 = existing === null ? "" : await readFile7(file.value, "utf8");
13817
14394
  const present = new Set(text3.split(/\r?\n/));
13818
14395
  const toAdd = missing.filter((entry) => !present.has(entry));
13819
14396
  if (toAdd.length === 0) {
@@ -13829,7 +14406,7 @@ async function excludeLocalFiles(input) {
13829
14406
  }
13830
14407
 
13831
14408
  // src/lib/instructions/hook-launcher.ts
13832
- import { mkdir as mkdir7, readFile as readFile7, rename as rename4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
14409
+ import { mkdir as mkdir8, readFile as readFile8, rename as rename4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
13833
14410
  import path23 from "path";
13834
14411
  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.";
13835
14412
  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.";
@@ -13850,15 +14427,15 @@ function samePath2(a, b) {
13850
14427
  return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
13851
14428
  }
13852
14429
  async function writeCopy(bytes, destination) {
13853
- await mkdir7(path23.dirname(destination), { recursive: true });
14430
+ await mkdir8(path23.dirname(destination), { recursive: true });
13854
14431
  const temporary = `${destination}.${process.pid}.tmp`;
13855
- await writeFile3(temporary, bytes, { mode: 420 });
14432
+ await writeFile4(temporary, bytes, { mode: 420 });
13856
14433
  try {
13857
14434
  await rename4(temporary, destination);
13858
14435
  return true;
13859
14436
  } catch (error) {
13860
14437
  await rm3(temporary, { force: true });
13861
- const existing = await readFile7(destination).catch(() => null);
14438
+ const existing = await readFile8(destination).catch(() => null);
13862
14439
  if (existing === null) {
13863
14440
  throw error;
13864
14441
  }
@@ -13881,7 +14458,7 @@ async function resolveHookLauncher(input) {
13881
14458
  return { prefix: `node ${word}`, warning: null };
13882
14459
  }
13883
14460
  const current = await writeCopy(
13884
- await readFile7(input.bundle.scriptPath),
14461
+ await readFile8(input.bundle.scriptPath),
13885
14462
  destination
13886
14463
  );
13887
14464
  return {
@@ -13934,7 +14511,7 @@ async function installHooks(input) {
13934
14511
  // src/lib/instructions/lesson-prompt.ts
13935
14512
  import { createHash as createHash11 } from "crypto";
13936
14513
  import { createReadStream } from "fs";
13937
- import { mkdir as mkdir8, readdir as readdir2, stat as stat5, unlink as unlink2, writeFile as writeFile4 } from "fs/promises";
14514
+ import { mkdir as mkdir9, readdir as readdir2, stat as stat5, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
13938
14515
  import path24 from "path";
13939
14516
  import { createInterface } from "readline";
13940
14517
  function parseStopHookInput(text3) {
@@ -14084,7 +14661,7 @@ async function runLessonPromptInner(input) {
14084
14661
  return { output: null, why: "stop-hook-active" };
14085
14662
  }
14086
14663
  try {
14087
- await mkdir8(input.markerDir, { recursive: true });
14664
+ await mkdir9(input.markerDir, { recursive: true });
14088
14665
  } catch (error) {
14089
14666
  debugLog(`marker directory unavailable: ${describeError(error)}`);
14090
14667
  return { output: null, why: "marker-unavailable" };
@@ -14108,7 +14685,7 @@ async function runLessonPromptInner(input) {
14108
14685
  return { output: null, why: "no-edits" };
14109
14686
  }
14110
14687
  try {
14111
- await writeFile4(markerPath, "", { flag: "wx" });
14688
+ await writeFile5(markerPath, "", { flag: "wx" });
14112
14689
  } catch (error) {
14113
14690
  debugLog(`could not write marker: ${describeError(error)}`);
14114
14691
  return { output: null, why: "marker-unavailable" };
@@ -14143,287 +14720,72 @@ var machine = {
14143
14720
  platform: () => process.platform
14144
14721
  };
14145
14722
 
14146
- // src/lib/instructions/open-proposals.ts
14147
- var openProposalTiming = { deadlineMs: 15e3 };
14148
- var SNAPSHOT_STATUSES = {
14149
- RECEIVING: true,
14150
- VALIDATING: true,
14151
- READY: true,
14152
- REJECTED: true,
14153
- FAILED: true
14154
- };
14155
- var PULL_REQUEST_STATES = {
14156
- QUEUED: true,
14157
- OPENING: true,
14158
- OPEN: true,
14159
- CLOSE_REQUESTED: true,
14160
- BLOCKED: true,
14161
- MERGED: true,
14162
- CLOSED: true,
14163
- CANCELED: true
14164
- };
14165
- function isKnown(known, value) {
14166
- return typeof value === "string" && Object.hasOwn(known, value);
14167
- }
14168
- var OpenProposalDeadline = class extends Error {
14169
- constructor(ms) {
14170
- super(`it did not answer within ${Math.ceil(ms / 1e3)} s`);
14171
- this.name = "OpenProposalDeadline";
14172
- }
14173
- };
14174
- async function lookUpOpenProposals(client, projectId, options = {}) {
14175
- const ms = openProposalTiming.deadlineMs;
14176
- const controller = new AbortController();
14177
- const timer = setTimeout(
14178
- () => controller.abort(new OpenProposalDeadline(ms)),
14179
- ms
14180
- );
14181
- let answer;
14182
- try {
14183
- answer = await client.instructions.getOpenProposals(projectId, {
14184
- org: options.org,
14185
- signal: controller.signal
14186
- });
14187
- } catch (error) {
14188
- if (controller.signal.aborted) {
14189
- return {
14190
- kind: "unavailable",
14191
- reason: describeError(controller.signal.reason)
14192
- };
14193
- }
14194
- if (error.status === 404) {
14195
- return {
14196
- kind: "unavailable",
14197
- reason: "this Fabric server does not list open proposals yet"
14198
- };
14199
- }
14200
- const status = error?.status;
14201
- return {
14202
- kind: "unavailable",
14203
- reason: typeof status === "number" && status > 0 ? `the server answered ${status}` : "the request did not go through"
14204
- };
14205
- } finally {
14206
- clearTimeout(timer);
14207
- }
14208
- if (!Array.isArray(answer) || !answer.every(isOpenProposal)) {
14209
- return {
14210
- kind: "unavailable",
14211
- reason: "the server's answer was not a list of proposals"
14212
- };
14723
+ // src/lib/instructions/native-push-plan.ts
14724
+ import { createHash as createHash13 } from "crypto";
14725
+
14726
+ // src/lib/instructions/push.ts
14727
+ import { createHash as createHash12 } from "crypto";
14728
+ var MAX_PUSH_CHANGES = 50;
14729
+ var MAX_INLINE_PUSH_BYTES = 2 * 1024 * 1024;
14730
+ function assertPushablePath(candidate, source) {
14731
+ const check = checkRelativePath(candidate);
14732
+ if (!check.ok) {
14733
+ throw new Error(
14734
+ `Refusing to push: ${describeRejection(check)}. ${source}`
14735
+ );
14213
14736
  }
14214
- return { kind: "found", proposals: answer };
14215
- }
14216
- function isOpenProposal(value) {
14217
- if (typeof value !== "object" || value === null) {
14218
- return false;
14737
+ if (isReservedPath(candidate)) {
14738
+ throw new Error(
14739
+ `Refusing to push: ${describeRejection({
14740
+ ok: false,
14741
+ reason: "reserved_path",
14742
+ detail: candidate
14743
+ })}. ${source}`
14744
+ );
14219
14745
  }
14220
- const proposal = value;
14221
- const pullRequest = proposal.pullRequest;
14222
- return typeof proposal.snapshotId === "string" && typeof proposal.version === "number" && typeof proposal.baseSnapshotId === "string" && isKnown(SNAPSHOT_STATUSES, proposal.status) && (pullRequest === null || typeof pullRequest === "object" && pullRequest !== void 0 && isKnown(PULL_REQUEST_STATES, pullRequest.state) && (pullRequest.url === null || typeof pullRequest.url === "string")) && Array.isArray(proposal.changes) && proposal.changes.every(isChange);
14223
14746
  }
14224
- function isChange(value) {
14225
- if (typeof value !== "object" || value === null) {
14226
- return false;
14747
+ function encodeContent(bytes) {
14748
+ const text3 = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
14749
+ const roundTrip = new TextEncoder().encode(text3);
14750
+ if (roundTrip.length === bytes.length && roundTrip.every((byte, index) => byte === bytes[index])) {
14751
+ return { content: text3, encoding: "utf8" };
14227
14752
  }
14228
- const change = value;
14229
- return typeof change.path === "string" && (change.op === "put" && typeof change.sha256 === "string" || change.op === "delete" && change.sha256 === null);
14753
+ return {
14754
+ content: Buffer.from(bytes).toString("base64"),
14755
+ encoding: "base64"
14756
+ };
14230
14757
  }
14231
-
14232
- // src/lib/instructions/prompt.ts
14233
- import { createInterface as createInterface2 } from "readline/promises";
14234
- function canPrompt(streams = {
14235
- stdin: process.stdin,
14236
- stderr: process.stderr
14237
- }) {
14238
- return streams.stdin?.isTTY === true && streams.stderr?.isTTY === true;
14758
+ function sha256Of(bytes) {
14759
+ return createHash12("sha256").update(bytes).digest("hex");
14239
14760
  }
14240
- async function ask(question, streams) {
14241
- const reader = createInterface2({
14242
- input: streams.input,
14243
- output: streams.output
14244
- });
14245
- try {
14246
- return (await reader.question(question)).trim();
14247
- } finally {
14248
- reader.close();
14249
- }
14761
+ function refuseStaleLedger(detail) {
14762
+ throw new Error(
14763
+ `Refusing to push: the sync lock does not match the published version \u2014 ${detail}. Run \`${fabricCommand("instructions sync")}\` and try again. Nothing was read and nothing was sent.`
14764
+ );
14250
14765
  }
14251
- async function chooseProject(projects, streams = { input: process.stdin, output: process.stderr }) {
14252
- streams.output.write(
14253
- `This repository is connected to ${projects.length} projects:
14254
- `
14766
+ function assertLockMatchesManifest(lock, manifest) {
14767
+ const published = new Map(
14768
+ manifest.map((entry) => [entry.path, entry.sha256])
14255
14769
  );
14256
- for (const [index, project] of projects.entries()) {
14257
- streams.output.write(` [${index + 1}] ${project.label}
14258
- `);
14770
+ for (const [path29, entry] of Object.entries(lock.files)) {
14771
+ const expected = published.get(path29);
14772
+ if (expected === void 0) {
14773
+ refuseStaleLedger(
14774
+ `it lists ${path29}, which version ${lock.snapshotVersion} does not contain`
14775
+ );
14776
+ }
14777
+ if (expected !== entry.sha256) {
14778
+ refuseStaleLedger(
14779
+ `its record of ${path29} is not the published file`
14780
+ );
14781
+ }
14259
14782
  }
14260
- const answer = await ask(`Which one? [1-${projects.length}] `, streams);
14261
- const number = /^\d+$/.test(answer) ? Number(answer) : 0;
14262
- return number >= 1 && number <= projects.length ? number - 1 : null;
14263
- }
14264
- async function confirm(question, streams = { input: process.stdin, output: process.stderr }) {
14265
- return /^y(es)?$/i.test(await ask(`${question} [y/N] `, streams));
14266
- }
14267
-
14268
- // src/lib/instructions/pull-request-wait.ts
14269
- var PULL_REQUEST_POLL_MS = 2e3;
14270
- var PULL_REQUEST_WAIT_MS = 6e4;
14271
- var SETTLED_STATES = /* @__PURE__ */ new Set([
14272
- "OPEN",
14273
- "BLOCKED",
14274
- "MERGED",
14275
- "CLOSED",
14276
- "CANCELED",
14277
- "CLOSE_REQUESTED"
14278
- ]);
14279
- var STOPPED_BRANCH_STATES = /* @__PURE__ */ new Set([
14280
- "BLOCKED",
14281
- "MERGED",
14282
- "CLOSED",
14283
- "CANCELED",
14284
- "CLOSE_REQUESTED"
14285
- ]);
14286
- function branchStoppedByFailure(branch) {
14287
- const failure = branch.failure;
14288
- if (failure === null) {
14289
- return false;
14290
- }
14291
- return failure.code === "REPOSITORY_CHANGED" || branch.pullRequest === null && !failure.retryable;
14292
- }
14293
- function isSettled(pullRequest) {
14294
- if (!SETTLED_STATES.has(pullRequest.state)) {
14295
- return false;
14296
- }
14297
- const branch = pullRequest.branch;
14298
- if (pullRequest.state === "OPEN" && branch) {
14299
- return branch.pullRequest?.state === "OPEN" || STOPPED_BRANCH_STATES.has(branch.state) || branchStoppedByFailure(branch);
14300
- }
14301
- return true;
14302
- }
14303
- var PullRequestWaitDeadline = class extends Error {
14304
- constructor() {
14305
- super("the wait for the pull request reached its deadline");
14306
- this.name = "PullRequestWaitDeadline";
14307
- }
14308
- };
14309
- function deadlineSignal(ms) {
14310
- const controller = new AbortController();
14311
- const timer = setTimeout(
14312
- () => controller.abort(new PullRequestWaitDeadline()),
14313
- ms
14314
- );
14315
- return { signal: controller.signal, clear: () => clearTimeout(timer) };
14316
- }
14317
- function delay(ms) {
14318
- return new Promise((resolve) => setTimeout(resolve, ms));
14319
- }
14320
- async function waitForPullRequest(client, projectId, snapshotId, options = {}) {
14321
- const deadline = Date.now() + PULL_REQUEST_WAIT_MS;
14322
- let last2 = null;
14323
- for (; ; ) {
14324
- const remaining = deadline - Date.now();
14325
- if (remaining <= 0) {
14326
- return { kind: "timed_out", pullRequest: last2 };
14327
- }
14328
- const expiry = deadlineSignal(remaining);
14329
- let pullRequest;
14330
- try {
14331
- pullRequest = await client.instructions.getProposalPullRequest(
14332
- projectId,
14333
- snapshotId,
14334
- { org: options.org, signal: expiry.signal }
14335
- );
14336
- } catch (error) {
14337
- if (expiry.signal.aborted) {
14338
- return { kind: "timed_out", pullRequest: last2 };
14339
- }
14340
- throw error;
14341
- } finally {
14342
- expiry.clear();
14343
- }
14344
- if (pullRequest === null) {
14345
- return { kind: "none" };
14346
- }
14347
- options.observe?.(pullRequest);
14348
- if (isSettled(pullRequest)) {
14349
- return { kind: "settled", pullRequest };
14350
- }
14351
- last2 = pullRequest;
14352
- if (deadline - Date.now() <= PULL_REQUEST_POLL_MS) {
14353
- return { kind: "timed_out", pullRequest: last2 };
14354
- }
14355
- await delay(PULL_REQUEST_POLL_MS);
14356
- }
14357
- }
14358
- function splitMessage(message) {
14359
- const [title = "", ...rest] = message.replace(/\r\n?/g, "\n").split("\n");
14360
- const body = rest.join("\n").replace(/^\n+/, "");
14361
- return body === "" ? { title } : { title, body };
14362
- }
14363
-
14364
- // src/lib/instructions/push.ts
14365
- import { createHash as createHash12 } from "crypto";
14366
- var MAX_PUSH_CHANGES = 50;
14367
- var MAX_INLINE_PUSH_BYTES = 2 * 1024 * 1024;
14368
- function assertPushablePath(candidate, source) {
14369
- const check = checkRelativePath(candidate);
14370
- if (!check.ok) {
14371
- throw new Error(
14372
- `Refusing to push: ${describeRejection(check)}. ${source}`
14373
- );
14374
- }
14375
- if (isReservedPath(candidate)) {
14376
- throw new Error(
14377
- `Refusing to push: ${describeRejection({
14378
- ok: false,
14379
- reason: "reserved_path",
14380
- detail: candidate
14381
- })}. ${source}`
14382
- );
14383
- }
14384
- }
14385
- function encodeContent(bytes) {
14386
- const text3 = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
14387
- const roundTrip = new TextEncoder().encode(text3);
14388
- if (roundTrip.length === bytes.length && roundTrip.every((byte, index) => byte === bytes[index])) {
14389
- return { content: text3, encoding: "utf8" };
14390
- }
14391
- return {
14392
- content: Buffer.from(bytes).toString("base64"),
14393
- encoding: "base64"
14394
- };
14395
- }
14396
- function sha256Of(bytes) {
14397
- return createHash12("sha256").update(bytes).digest("hex");
14398
- }
14399
- function refuseStaleLedger(detail) {
14400
- throw new Error(
14401
- `Refusing to push: the sync lock does not match the published version \u2014 ${detail}. Run \`${fabricCommand("instructions sync")}\` and try again. Nothing was read and nothing was sent.`
14402
- );
14403
- }
14404
- function assertLockMatchesManifest(lock, manifest) {
14405
- const published = new Map(
14406
- manifest.map((entry) => [entry.path, entry.sha256])
14407
- );
14408
- for (const [path29, entry] of Object.entries(lock.files)) {
14409
- const expected = published.get(path29);
14410
- if (expected === void 0) {
14411
- refuseStaleLedger(
14412
- `it lists ${path29}, which version ${lock.snapshotVersion} does not contain`
14413
- );
14414
- }
14415
- if (expected !== entry.sha256) {
14416
- refuseStaleLedger(
14417
- `its record of ${path29} is not the published file`
14418
- );
14419
- }
14420
- }
14421
- for (const entry of manifest) {
14422
- if (!Object.hasOwn(lock.files, entry.path)) {
14423
- refuseStaleLedger(
14424
- `version ${lock.snapshotVersion} contains ${entry.path}, which the lock does not list`
14425
- );
14426
- }
14783
+ for (const entry of manifest) {
14784
+ if (!Object.hasOwn(lock.files, entry.path)) {
14785
+ refuseStaleLedger(
14786
+ `version ${lock.snapshotVersion} contains ${entry.path}, which the lock does not list`
14787
+ );
14788
+ }
14427
14789
  }
14428
14790
  }
14429
14791
  async function computePushPlan(input) {
@@ -14501,95 +14863,731 @@ async function computePushPlan(input) {
14501
14863
  sha256: fingerprint.sha256
14502
14864
  });
14503
14865
  }
14504
- return { entries, unchanged };
14866
+ return { entries, unchanged };
14867
+ }
14868
+ function pushContentBytes(entries) {
14869
+ return entries.reduce(
14870
+ (total, entry) => total + (entry.action === "put" ? entry.size : 0),
14871
+ 0
14872
+ );
14873
+ }
14874
+ async function materializePushChanges(input) {
14875
+ if (input.entries.length > MAX_PUSH_CHANGES) {
14876
+ throw new Error(`Too many changes to push (${input.entries.length}).`);
14877
+ }
14878
+ if (pushContentBytes(input.entries) > MAX_INLINE_PUSH_BYTES) {
14879
+ throw new Error("The selected changes are too large to send inline.");
14880
+ }
14881
+ const changes = [];
14882
+ for (const entry of input.entries) {
14883
+ if (entry.action === "delete") {
14884
+ const current = await readFileSafely(input.root, entry.path, {
14885
+ maxBytes: 0
14886
+ });
14887
+ if (current !== null) {
14888
+ throw new Error(
14889
+ `Refusing to push: ${entry.path} reappeared after planning. Plan the changes again.`
14890
+ );
14891
+ }
14892
+ changes.push({ op: "delete", path: entry.path });
14893
+ continue;
14894
+ }
14895
+ const read = await readFileSafely(input.root, entry.path, {
14896
+ maxBytes: MAX_INLINE_PUSH_BYTES
14897
+ });
14898
+ if (read === null) {
14899
+ throw new Error(
14900
+ `Refusing to push: ${entry.path} disappeared after planning. Run \`${fabricCommand("instructions push")}\` again.`
14901
+ );
14902
+ }
14903
+ const sha256 = sha256Of(read.bytes);
14904
+ if (read.bytes.length !== entry.size || sha256 !== entry.sha256) {
14905
+ throw new Error(
14906
+ `Refusing to push: ${entry.path} changed after planning. Run \`${fabricCommand("instructions push")}\` again.`
14907
+ );
14908
+ }
14909
+ changes.push({
14910
+ op: "put",
14911
+ path: entry.path,
14912
+ ...encodeContent(read.bytes)
14913
+ });
14914
+ }
14915
+ return changes;
14916
+ }
14917
+ var LIVE_PULL_REQUEST_STATES = /* @__PURE__ */ new Set([
14918
+ "QUEUED",
14919
+ "OPENING",
14920
+ "OPEN"
14921
+ ]);
14922
+ var LIVE_SNAPSHOT_STATUSES = /* @__PURE__ */ new Set([
14923
+ "VALIDATING",
14924
+ "READY"
14925
+ ]);
14926
+ function carryingProposals(proposals, base) {
14927
+ return proposals.filter(
14928
+ (proposal) => (typeof base === "string" ? proposal.kind !== "native" && proposal.baseSnapshotId === base : proposal.kind === "native" && proposal.nativeBase.generation === base.generation && proposal.nativeBase.commitSha === base.commitSha) && LIVE_SNAPSHOT_STATUSES.has(proposal.status) && (proposal.pullRequest === null || LIVE_PULL_REQUEST_STATES.has(proposal.pullRequest.state))
14929
+ ).sort(
14930
+ (a, b) => a.kind === "native" || b.kind === "native" ? 0 : b.version - a.version
14931
+ );
14932
+ }
14933
+ function setAsideProposed(plan, proposals, base) {
14934
+ const carrying = carryingProposals(proposals, base);
14935
+ const carriedBy = (entry) => carrying.find(
14936
+ (proposal) => proposal.changes.some(
14937
+ (change) => change.path === entry.path && change.op === entry.action && (entry.action === "delete" || change.sha256 === entry.sha256)
14938
+ )
14939
+ );
14940
+ const setAside = /* @__PURE__ */ new Set();
14941
+ const alreadyProposed = [];
14942
+ for (const entry of plan.entries) {
14943
+ const proposal = carriedBy(entry);
14944
+ if (proposal === void 0) {
14945
+ continue;
14946
+ }
14947
+ setAside.add(entry.path);
14948
+ alreadyProposed.push({
14949
+ path: entry.path,
14950
+ action: entry.action,
14951
+ proposal: {
14952
+ snapshotId: proposal.kind === "native" ? proposal.operationId : proposal.snapshotId,
14953
+ version: proposal.kind === "native" ? null : proposal.version,
14954
+ pullRequest: proposal.pullRequest
14955
+ }
14956
+ });
14957
+ }
14958
+ return {
14959
+ plan: {
14960
+ entries: plan.entries.filter((entry) => !setAside.has(entry.path)),
14961
+ unchanged: plan.unchanged
14962
+ },
14963
+ alreadyProposed
14964
+ };
14965
+ }
14966
+
14967
+ // src/lib/instructions/native-push-plan.ts
14968
+ async function computeNativePushPlan(input) {
14969
+ const allowed = new Set(input.files.map((file) => file.path));
14970
+ const selected = new Set(
14971
+ input.changedPaths.filter((file) => allowed.has(file))
14972
+ );
14973
+ for (const file of input.added ?? []) {
14974
+ assertPushablePath(file, "That path cannot be pushed.");
14975
+ if (!allowed.has(file)) selected.add(file);
14976
+ }
14977
+ if (selected.size > MAX_PUSH_CHANGES) {
14978
+ throw new Error(
14979
+ `Too many changes to push (${selected.size} > ${MAX_PUSH_CHANGES}). Select a smaller change set.`
14980
+ );
14981
+ }
14982
+ const collision = findCollision([.../* @__PURE__ */ new Set([...allowed, ...selected])]);
14983
+ if (collision !== null) {
14984
+ throw new Error(
14985
+ `Refusing to push colliding paths: ${collision.first} and ${collision.second}.`
14986
+ );
14987
+ }
14988
+ const entries = [];
14989
+ let totalBytes = 0;
14990
+ for (const file of [...selected].sort()) {
14991
+ assertPushablePath(file, "That path cannot be pushed.");
14992
+ const read = await readFileSafely(input.root, file, {
14993
+ maxBytes: MAX_INLINE_PUSH_BYTES - totalBytes
14994
+ });
14995
+ if (read === null) {
14996
+ if (!allowed.has(file))
14997
+ throw new Error(
14998
+ `Refusing to push: there is no file at ${file}.`
14999
+ );
15000
+ entries.push({ path: file, action: "delete" });
15001
+ } else {
15002
+ totalBytes += read.bytes.length;
15003
+ entries.push({
15004
+ path: file,
15005
+ action: "put",
15006
+ size: read.bytes.length,
15007
+ sha256: createHash13("sha256").update(read.bytes).digest("hex")
15008
+ });
15009
+ }
15010
+ }
15011
+ return {
15012
+ entries,
15013
+ unchanged: input.files.filter((file) => !selected.has(file.path)).map((file) => file.path)
15014
+ };
15015
+ }
15016
+
15017
+ // src/lib/instructions/open-proposals.ts
15018
+ var openProposalTiming = { deadlineMs: 15e3 };
15019
+ var SNAPSHOT_STATUSES = {
15020
+ RECEIVING: true,
15021
+ VALIDATING: true,
15022
+ READY: true,
15023
+ REJECTED: true,
15024
+ FAILED: true
15025
+ };
15026
+ var PULL_REQUEST_STATES = {
15027
+ QUEUED: true,
15028
+ OPENING: true,
15029
+ OPEN: true,
15030
+ CLOSE_REQUESTED: true,
15031
+ BLOCKED: true,
15032
+ MERGED: true,
15033
+ CLOSED: true,
15034
+ CANCELED: true
15035
+ };
15036
+ function isKnown(known, value) {
15037
+ return typeof value === "string" && Object.hasOwn(known, value);
15038
+ }
15039
+ var OpenProposalDeadline = class extends Error {
15040
+ constructor(ms) {
15041
+ super(`it did not answer within ${Math.ceil(ms / 1e3)} s`);
15042
+ this.name = "OpenProposalDeadline";
15043
+ }
15044
+ };
15045
+ async function lookUpOpenProposals(client, projectId, options = {}) {
15046
+ const ms = openProposalTiming.deadlineMs;
15047
+ const controller = new AbortController();
15048
+ const timer = setTimeout(
15049
+ () => controller.abort(new OpenProposalDeadline(ms)),
15050
+ ms
15051
+ );
15052
+ let answer;
15053
+ try {
15054
+ answer = await client.instructions.getOpenProposals(projectId, {
15055
+ org: options.org,
15056
+ signal: controller.signal
15057
+ });
15058
+ } catch (error) {
15059
+ if (controller.signal.aborted) {
15060
+ return {
15061
+ kind: "unavailable",
15062
+ reason: describeError(controller.signal.reason)
15063
+ };
15064
+ }
15065
+ if (error.status === 404) {
15066
+ return {
15067
+ kind: "unavailable",
15068
+ reason: "this Fabric server does not list open proposals yet"
15069
+ };
15070
+ }
15071
+ const status = error?.status;
15072
+ return {
15073
+ kind: "unavailable",
15074
+ reason: typeof status === "number" && status > 0 ? `the server answered ${status}` : "the request did not go through"
15075
+ };
15076
+ } finally {
15077
+ clearTimeout(timer);
15078
+ }
15079
+ if (!Array.isArray(answer) || !answer.every(isOpenProposal)) {
15080
+ return {
15081
+ kind: "unavailable",
15082
+ reason: "the server's answer was not a list of proposals"
15083
+ };
15084
+ }
15085
+ return { kind: "found", proposals: answer };
15086
+ }
15087
+ function isOpenProposal(value) {
15088
+ if (typeof value !== "object" || value === null) {
15089
+ return false;
15090
+ }
15091
+ const proposal = value;
15092
+ const pullRequest = proposal.pullRequest;
15093
+ return (proposal.kind === "native" ? typeof proposal.operationId === "string" && isNativeBase(proposal.nativeBase) : typeof proposal.snapshotId === "string" && typeof proposal.version === "number" && typeof proposal.baseSnapshotId === "string") && isKnown(SNAPSHOT_STATUSES, proposal.status) && (pullRequest === null || typeof pullRequest === "object" && pullRequest !== void 0 && isKnown(PULL_REQUEST_STATES, pullRequest.state) && (pullRequest.url === null || typeof pullRequest.url === "string")) && Array.isArray(proposal.changes) && proposal.changes.every(isChange);
15094
+ }
15095
+ function isNativeBase(value) {
15096
+ if (typeof value !== "object" || value === null) return false;
15097
+ return "generation" in value && Number.isSafeInteger(value.generation) && typeof value.generation === "number" && value.generation > 0 && "commitSha" in value && typeof value.commitSha === "string" && /^[a-f0-9]{40}$/.test(value.commitSha);
15098
+ }
15099
+ function isChange(value) {
15100
+ if (typeof value !== "object" || value === null) {
15101
+ return false;
15102
+ }
15103
+ const change = value;
15104
+ return typeof change.path === "string" && (change.op === "put" && typeof change.sha256 === "string" || change.op === "delete" && change.sha256 === null);
15105
+ }
15106
+
15107
+ // src/lib/instructions/pull-request-wait.ts
15108
+ var PULL_REQUEST_POLL_MS = 2e3;
15109
+ var PULL_REQUEST_WAIT_MS = 6e4;
15110
+ var SETTLED_STATES = /* @__PURE__ */ new Set([
15111
+ "OPEN",
15112
+ "BLOCKED",
15113
+ "MERGED",
15114
+ "CLOSED",
15115
+ "CANCELED",
15116
+ "CLOSE_REQUESTED"
15117
+ ]);
15118
+ var STOPPED_BRANCH_STATES = /* @__PURE__ */ new Set([
15119
+ "BLOCKED",
15120
+ "MERGED",
15121
+ "CLOSED",
15122
+ "CANCELED",
15123
+ "CLOSE_REQUESTED"
15124
+ ]);
15125
+ function branchStoppedByFailure(branch) {
15126
+ const failure = branch.failure;
15127
+ if (failure === null) {
15128
+ return false;
15129
+ }
15130
+ return failure.code === "REPOSITORY_CHANGED" || branch.pullRequest === null && !failure.retryable;
15131
+ }
15132
+ function isSettled(pullRequest) {
15133
+ if (!SETTLED_STATES.has(pullRequest.state)) {
15134
+ return false;
15135
+ }
15136
+ const branch = pullRequest.branch;
15137
+ if (pullRequest.state === "OPEN" && branch) {
15138
+ return branch.pullRequest?.state === "OPEN" || STOPPED_BRANCH_STATES.has(branch.state) || branchStoppedByFailure(branch);
15139
+ }
15140
+ return true;
15141
+ }
15142
+ var PullRequestWaitDeadline = class extends Error {
15143
+ constructor() {
15144
+ super("the wait for the pull request reached its deadline");
15145
+ this.name = "PullRequestWaitDeadline";
15146
+ }
15147
+ };
15148
+ function deadlineSignal(ms) {
15149
+ const controller = new AbortController();
15150
+ const timer = setTimeout(
15151
+ () => controller.abort(new PullRequestWaitDeadline()),
15152
+ ms
15153
+ );
15154
+ return { signal: controller.signal, clear: () => clearTimeout(timer) };
15155
+ }
15156
+ function delay(ms) {
15157
+ return new Promise((resolve) => setTimeout(resolve, ms));
15158
+ }
15159
+ async function waitForPullRequest(client, projectId, snapshotId, options = {}) {
15160
+ const deadline = Date.now() + PULL_REQUEST_WAIT_MS;
15161
+ let last2 = null;
15162
+ for (; ; ) {
15163
+ const remaining = deadline - Date.now();
15164
+ if (remaining <= 0) {
15165
+ return { kind: "timed_out", pullRequest: last2 };
15166
+ }
15167
+ const expiry = deadlineSignal(remaining);
15168
+ let pullRequest;
15169
+ try {
15170
+ pullRequest = await client.instructions.getProposalPullRequest(
15171
+ projectId,
15172
+ snapshotId,
15173
+ { org: options.org, signal: expiry.signal }
15174
+ );
15175
+ } catch (error) {
15176
+ if (expiry.signal.aborted) {
15177
+ return { kind: "timed_out", pullRequest: last2 };
15178
+ }
15179
+ throw error;
15180
+ } finally {
15181
+ expiry.clear();
15182
+ }
15183
+ if (pullRequest === null) {
15184
+ return { kind: "none" };
15185
+ }
15186
+ options.observe?.(pullRequest);
15187
+ if (isSettled(pullRequest)) {
15188
+ return { kind: "settled", pullRequest };
15189
+ }
15190
+ last2 = pullRequest;
15191
+ if (deadline - Date.now() <= PULL_REQUEST_POLL_MS) {
15192
+ return { kind: "timed_out", pullRequest: last2 };
15193
+ }
15194
+ await delay(PULL_REQUEST_POLL_MS);
15195
+ }
15196
+ }
15197
+ function splitMessage(message) {
15198
+ const [title = "", ...rest] = message.replace(/\r\n?/g, "\n").split("\n");
15199
+ const body = rest.join("\n").replace(/^\n+/, "");
15200
+ return body === "" ? { title } : { title, body };
15201
+ }
15202
+
15203
+ // src/lib/instructions/pull-request-verdict.ts
15204
+ function pushPullRequest(pullRequest, timedOut) {
15205
+ const branch = pullRequest.branch ?? null;
15206
+ return {
15207
+ operationId: pullRequest.operationId,
15208
+ state: pullRequest.state,
15209
+ url: branch?.pullRequest?.url ?? pullRequest.url,
15210
+ failure: pullRequest.failure,
15211
+ timedOut,
15212
+ branch,
15213
+ append: pullRequest.append ?? null
15214
+ };
15215
+ }
15216
+ var BLOCKED_REASONS = {
15217
+ ATTRIBUTION_REJECTED: "the project's name or your display name looks like an address, a link or a credential, so the commit could not name you safely",
15218
+ PERMISSION_REVOKED: "you no longer have permission to suggest changes to this project's repository",
15219
+ CONFIGURATION_CHANGED: "the project's repository settings changed after you suggested this; push again",
15220
+ TARGET_BRANCH_MISSING: "the repository branch the project syncs from no longer exists",
15221
+ BASE_COMMIT_UNAVAILABLE: "the commit this suggestion was based on is no longer in the repository; sync and push again",
15222
+ TREE_CONFLICT: "the repository changed the same files since your last sync; sync and push again",
15223
+ AUTHENTICATION_FAILED: "the project's repository connection needs to be reconnected",
15224
+ REPOSITORY_UNAVAILABLE: "the repository could not be reached",
15225
+ BRANCH_WRITE_REFUSED: "the repository refused the new branch",
15226
+ PR_CREATION_REFUSED: "the repository refused to open the pull request",
15227
+ REMOTE_REF_CONFLICT: "a branch with the same name already exists in the repository",
15228
+ CREATE_OUTCOME_UNKNOWN: "Fabric could not confirm whether the pull request was created",
15229
+ VALIDATION_TIMEOUT: "the files' checks did not finish in time",
15230
+ LIMITS_EXCEEDED: "the change is larger than Fabric can open as a pull request",
15231
+ PROVIDER_RATE_LIMITED: "the repository's provider is limiting requests",
15232
+ PROVIDER_TEMPORARY: "the repository's provider had a temporary problem",
15233
+ LOOKUP_INCONCLUSIVE: "Fabric could not confirm the pull request's state",
15234
+ // Member proposal branches (Fizzy #2738 spec §9). BRANCH_CONFLICT and
15235
+ // SUPERSEDED_BY_LATER_CHANGE name their paths, in `blockedReason`.
15236
+ BRANCH_MOVED: "your branch kept changing while Fabric was adding to it",
15237
+ BRANCH_NAME_UNAVAILABLE: "Fabric could not reserve a branch name for your pull request; ask an owner to remove old proposal branches",
15238
+ REPOSITORY_CHANGED: "the repository connection now points at another repository, so Fabric can no longer reach your pull request; close it in the repository, then stop tracking it in the Coding Instructions tab",
15239
+ PUSH_OUTCOME_UNKNOWN: "Fabric could not confirm whether this change reached your branch, which may have been changed outside Fabric; check the branch, then try again or withdraw it",
15240
+ BRANCH_MISSING: "your branch was deleted in the repository"
15241
+ };
15242
+ var FAILURE_PATHS_LISTED = 20;
15243
+ function failurePaths(failure) {
15244
+ const paths = failure.params?.paths;
15245
+ const count = failure.params?.count;
15246
+ if (typeof paths !== "string" || paths === "") {
15247
+ return "some of its files";
15248
+ }
15249
+ return typeof count === "number" && count > FAILURE_PATHS_LISTED ? `${paths} and ${count - FAILURE_PATHS_LISTED} more` : paths;
15250
+ }
15251
+ function blockedReason(failure) {
15252
+ switch (failure.code) {
15253
+ case "BRANCH_CONFLICT":
15254
+ return `files on your branch were changed outside Fabric (${failurePaths(failure)}), and Fabric does not overwrite them while the pull request is open; make this change on your branch in the repository, or wait until the pull request merges`;
15255
+ case "SUPERSEDED_BY_LATER_CHANGE":
15256
+ return `a newer change of yours already edits ${failurePaths(failure)}; use Try again to apply this one on top of it, or withdraw it`;
15257
+ default:
15258
+ return BLOCKED_REASONS[failure.code] ?? `it stopped with ${failure.code}`;
15259
+ }
15260
+ }
15261
+ function pullRequestVerdict(pullRequest, { waited, openedHere }) {
15262
+ const url = pullRequest.url ? `: ${pullRequest.url}` : "";
15263
+ const branch = pullRequest.branch;
15264
+ if (!waited) {
15265
+ return {
15266
+ text: "Follow its pull request in the project's Coding Instructions tab.",
15267
+ fails: false
15268
+ };
15269
+ }
15270
+ if (pullRequest.timedOut) {
15271
+ return {
15272
+ text: "The pull request is being opened; see the project's Coding Instructions tab.",
15273
+ fails: false
15274
+ };
15275
+ }
15276
+ switch (pullRequest.state) {
15277
+ case "OPEN": {
15278
+ if (!branch) {
15279
+ return { text: `Pull request opened${url}`, fails: false };
15280
+ }
15281
+ if (branch.state === "MERGED") {
15282
+ return {
15283
+ text: `Your pull request was merged${url} while this change was being added to it; see the project's Coding Instructions tab for whether it carried this change.`,
15284
+ fails: false
15285
+ };
15286
+ }
15287
+ if (branch.state === "CLOSED") {
15288
+ return {
15289
+ text: `Your pull request was closed without merging${url} while this change was being added to it.`,
15290
+ fails: false
15291
+ };
15292
+ }
15293
+ if (branch.failure && (branch.state === "BLOCKED" || branchStoppedByFailure(branch))) {
15294
+ const next = branch.failure.retryable ? "Fabric will try again on its own; see" : "See";
15295
+ const what = branch.pullRequest ? `Your change is on your branch, but Fabric can no longer update its pull request${url}` : "Your change is on your branch, but Fabric could not open its pull request";
15296
+ return {
15297
+ text: `${what}: ${blockedReason(branch.failure)}. ${next} the project's Coding Instructions tab.`,
15298
+ fails: true
15299
+ };
15300
+ }
15301
+ if (branch.state === "CLOSE_REQUESTED") {
15302
+ return {
15303
+ text: "Your pull request is being closed, and this change leaves with it; push again once it has closed.",
15304
+ fails: true
15305
+ };
15306
+ }
15307
+ if (branch.state === "CANCELED") {
15308
+ return {
15309
+ text: "Your branch was closed before its pull request opened; push again.",
15310
+ fails: true
15311
+ };
15312
+ }
15313
+ if (branch.pullRequest?.state === "OPEN") {
15314
+ return {
15315
+ text: openedHere ? `Opened pull request ${branch.pullRequest.url}` : `Added to your pull request ${branch.pullRequest.url}`,
15316
+ fails: false
15317
+ };
15318
+ }
15319
+ return {
15320
+ text: "Your change is on your branch and its pull request is being opened; see the project's Coding Instructions tab.",
15321
+ fails: false
15322
+ };
15323
+ }
15324
+ case "MERGED":
15325
+ return {
15326
+ text: `Its pull request was already merged${url}. Run \`${fabricCommand("instructions sync")}\` once the project has synced it.`,
15327
+ fails: false
15328
+ };
15329
+ case "CLOSED":
15330
+ return {
15331
+ text: `Its pull request was closed without merging${url}.`,
15332
+ fails: false
15333
+ };
15334
+ case "CANCELED":
15335
+ if (pullRequest.failure?.code === "ALREADY_ON_BRANCH") {
15336
+ return {
15337
+ text: branch?.pullRequest ? `Already on your branch; nothing to add. Your pull request: ${branch.pullRequest.url}` : "Already on your branch; nothing to add.",
15338
+ fails: false
15339
+ };
15340
+ }
15341
+ return {
15342
+ text: pullRequest.failure?.code === "VALIDATION_REJECTED" ? "The suggestion did not pass Fabric's checks, so no pull request was opened; see the project's Coding Instructions tab." : branch ? "The suggestion was withdrawn before it was added to your pull request." : "The suggestion was withdrawn before a pull request was opened.",
15343
+ fails: true
15344
+ };
15345
+ case "CLOSE_REQUESTED":
15346
+ return {
15347
+ text: "An earlier attempt at this change was withdrawn and its pull request is being closed; push again once it has closed.",
15348
+ fails: true
15349
+ };
15350
+ case "BLOCKED": {
15351
+ const failure = pullRequest.failure ?? branch?.failure ?? null;
15352
+ const reason = failure ? blockedReason(failure) : "it stopped with an unknown failure";
15353
+ const next = failure?.retryable ? "Fabric will try again on its own; see" : "See";
15354
+ const what = branch ? "Fabric could not add this change to your pull request" : "Fabric could not open the pull request";
15355
+ return {
15356
+ text: `${what}: ${reason}. ${next} the project's Coding Instructions tab.`,
15357
+ fails: true
15358
+ };
15359
+ }
15360
+ default:
15361
+ return {
15362
+ text: "The pull request is being opened; see the project's Coding Instructions tab.",
15363
+ fails: false
15364
+ };
15365
+ }
14505
15366
  }
14506
- function pushContentBytes(entries) {
14507
- return entries.reduce(
14508
- (total, entry) => total + (entry.action === "put" ? entry.size : 0),
14509
- 0
15367
+
15368
+ // src/lib/instructions/native-push.ts
15369
+ async function runNativeInstructionPush(input) {
15370
+ if (input.publish)
15371
+ throw new CliFailure(
15372
+ "Repository instructions are changed through Git commits or reviewed suggestions. Run push without --publish to open a suggestion.",
15373
+ 2
15374
+ );
15375
+ const deadline = Date.now() + 3e4;
15376
+ const checkout = await classifyCheckout({
15377
+ destination: input.root,
15378
+ repository: input.repository,
15379
+ deadline,
15380
+ remote: input.remote
15381
+ });
15382
+ if (checkout.class !== "matching")
15383
+ throw new CliFailure(
15384
+ "Run this command in the attached repository's instruction folder. No files were sent.",
15385
+ 7
15386
+ );
15387
+ const head = await headSha(checkout.toplevel, deadline);
15388
+ if (head.kind !== "ok" || head.value !== input.direct.currentCommitSha) {
15389
+ throw new CliFailure(
15390
+ "This checkout's HEAD does not match the repository revision read through Fabric. Update the checkout before proposing working-tree changes. Local edits were preserved.",
15391
+ 7
15392
+ );
15393
+ }
15394
+ const operation = await operationInProgress(
15395
+ checkout.toplevel,
15396
+ deadline
14510
15397
  );
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}).`);
15398
+ if (operation.kind !== "ok" || operation.value !== null) {
15399
+ throw new CliFailure(
15400
+ "Finish the current Git operation before proposing changes. No files were sent.",
15401
+ 7
15402
+ );
14515
15403
  }
14516
- if (pushContentBytes(input.entries) > MAX_INLINE_PUSH_BYTES) {
14517
- throw new Error("The selected changes are too large to send inline.");
15404
+ const changed = await changedTrackedPaths(input.root, deadline);
15405
+ if (changed.kind !== "ok")
15406
+ throw new CliFailure(
15407
+ "Git could not list this checkout's changes. No files were sent.",
15408
+ 7
15409
+ );
15410
+ const nativeBase = {
15411
+ generation: input.direct.generation,
15412
+ commitSha: input.direct.currentCommitSha
15413
+ };
15414
+ const listed = await input.client.instructions.listRepositoryFiles(
15415
+ input.projectId,
15416
+ { ...nativeBase, org: input.org }
15417
+ );
15418
+ if (listed.generation !== nativeBase.generation || listed.commitSha !== nativeBase.commitSha || listed.incomplete || listed.refusal !== null) {
15419
+ throw new CliFailure(
15420
+ "The repository tree could not be read completely at the selected revision. Refresh and try again; no files were sent.",
15421
+ 7
15422
+ );
14518
15423
  }
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;
15424
+ let plan = await computeNativePushPlan({
15425
+ root: input.root,
15426
+ files: listed.files,
15427
+ changedPaths: changed.value,
15428
+ added: input.add
15429
+ });
15430
+ if (plan.entries.length === 0)
15431
+ throw new CliFailure(
15432
+ "Nothing to push: Git reports no instruction content changes. Add a new file with --add <path>.",
15433
+ 7
15434
+ );
15435
+ let alreadyProposed = [];
15436
+ let openProposalCheck = "skipped";
15437
+ if (!input.includeProposed) {
15438
+ const lookup = await lookUpOpenProposals(
15439
+ input.client,
15440
+ input.projectId,
15441
+ { org: input.org }
15442
+ );
15443
+ if (lookup.kind === "found") {
15444
+ ({ plan, alreadyProposed } = setAsideProposed(
15445
+ plan,
15446
+ lookup.proposals,
15447
+ nativeBase
15448
+ ));
15449
+ openProposalCheck = "checked";
15450
+ } else {
15451
+ openProposalCheck = "unavailable";
15452
+ process.stderr.write(
15453
+ `Could not check your open proposals (${lookup.reason}); the selected changes will be sent.
15454
+ `
15455
+ );
14524
15456
  }
14525
- const read = await readFileSafely(input.root, entry.path, {
14526
- maxBytes: MAX_INLINE_PUSH_BYTES
15457
+ }
15458
+ const outcome = {
15459
+ projectId: input.projectId,
15460
+ destination: input.root,
15461
+ nativeBase,
15462
+ dryRun: Boolean(input.dryRun),
15463
+ put: plan.entries.filter((entry) => entry.action === "put").map((entry) => entry.path),
15464
+ deleted: plan.entries.filter((entry) => entry.action === "delete").map((entry) => entry.path),
15465
+ unchanged: plan.unchanged.length,
15466
+ openProposalCheck,
15467
+ alreadyProposed: alreadyProposed.map((entry) => ({
15468
+ path: entry.path,
15469
+ operationId: entry.proposal.snapshotId,
15470
+ pullRequestUrl: entry.proposal.pullRequest?.url ?? null
15471
+ })),
15472
+ operationId: null,
15473
+ pullRequest: null,
15474
+ timedOut: false
15475
+ };
15476
+ let sawBranchWithoutPullRequest = false;
15477
+ if (!input.dryRun && plan.entries.length > 0) {
15478
+ const changes = await materializePushChanges({
15479
+ root: input.root,
15480
+ entries: plan.entries
14527
15481
  });
14528
- if (read === null) {
14529
- throw new Error(
14530
- `Refusing to push: ${entry.path} disappeared after planning. Run \`${fabricCommand("instructions push")}\` again.`
15482
+ const currentHead = await headSha(checkout.toplevel, deadline);
15483
+ if (currentHead.kind !== "ok" || currentHead.value !== nativeBase.commitSha)
15484
+ throw new CliFailure(
15485
+ "HEAD changed while planning. No files were sent; run push again.",
15486
+ 7
14531
15487
  );
15488
+ const submitted = await input.client.instructions.submitRepositoryChange(
15489
+ input.projectId,
15490
+ nativeBase,
15491
+ changes,
15492
+ {
15493
+ org: input.org,
15494
+ ...input.message === void 0 ? {} : { note: splitMessage(input.message) }
15495
+ }
15496
+ );
15497
+ outcome.operationId = submitted.operationId;
15498
+ if (input.wait !== false) {
15499
+ try {
15500
+ const waited = await waitForPullRequest(
15501
+ input.client,
15502
+ input.projectId,
15503
+ submitted.operationId,
15504
+ {
15505
+ org: input.org,
15506
+ observe: (status) => {
15507
+ if (status.branch && status.branch.pullRequest === null)
15508
+ sawBranchWithoutPullRequest = true;
15509
+ }
15510
+ }
15511
+ );
15512
+ if (waited.kind !== "none") {
15513
+ outcome.pullRequest = waited.pullRequest ? pushPullRequest(
15514
+ waited.pullRequest,
15515
+ waited.kind === "timed_out"
15516
+ ) : null;
15517
+ outcome.timedOut = waited.kind === "timed_out";
15518
+ }
15519
+ } catch (error) {
15520
+ throw new CliFailure(
15521
+ `Suggestion ${submitted.operationId} was accepted, but its pull request could not be checked: ${describeError(error)}. Check the project's Coding Instructions tab.`,
15522
+ 7
15523
+ );
15524
+ }
14532
15525
  }
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.`
15526
+ }
15527
+ const verdict = outcome.pullRequest ? pullRequestVerdict(outcome.pullRequest, {
15528
+ waited: input.wait !== false,
15529
+ openedHere: sawBranchWithoutPullRequest
15530
+ }) : null;
15531
+ if (input.format === "json") printOutput(outcome, { format: "json" });
15532
+ else {
15533
+ if (input.dryRun)
15534
+ process.stdout.write(
15535
+ `Would suggest ${plan.entries.length} changes against Git ${nativeBase.commitSha.slice(0, 7)}.
15536
+ `
14537
15537
  );
14538
- }
14539
- changes.push({
14540
- op: "put",
14541
- path: entry.path,
14542
- ...encodeContent(read.bytes)
14543
- });
15538
+ else if (outcome.operationId)
15539
+ process.stdout.write(
15540
+ `Suggestion accepted: ${outcome.operationId}.
15541
+ `
15542
+ );
15543
+ else
15544
+ process.stdout.write(
15545
+ "These changes are already in your open suggestions.\n"
15546
+ );
15547
+ for (const entry of plan.entries)
15548
+ process.stdout.write(` ${entry.action} ${entry.path}
15549
+ `);
15550
+ if (verdict && !verdict.fails)
15551
+ process.stdout.write(`${verdict.text}
15552
+ `);
14544
15553
  }
14545
- return changes;
15554
+ if (verdict?.fails) throw new CliFailure(verdict.text, 7);
14546
15555
  }
14547
- var LIVE_PULL_REQUEST_STATES = /* @__PURE__ */ new Set([
14548
- "QUEUED",
14549
- "OPENING",
14550
- "OPEN"
14551
- ]);
14552
- var LIVE_SNAPSHOT_STATUSES = /* @__PURE__ */ new Set([
14553
- "VALIDATING",
14554
- "READY"
14555
- ]);
14556
- function carryingProposals(proposals, baseSnapshotId) {
14557
- return proposals.filter(
14558
- (proposal) => proposal.baseSnapshotId === baseSnapshotId && LIVE_SNAPSHOT_STATUSES.has(proposal.status) && (proposal.pullRequest === null || LIVE_PULL_REQUEST_STATES.has(proposal.pullRequest.state))
14559
- ).sort((a, b) => b.version - a.version);
15556
+
15557
+ // src/lib/instructions/prompt.ts
15558
+ import { createInterface as createInterface2 } from "readline/promises";
15559
+ function canPrompt(streams = {
15560
+ stdin: process.stdin,
15561
+ stderr: process.stderr
15562
+ }) {
15563
+ return streams.stdin?.isTTY === true && streams.stderr?.isTTY === true;
14560
15564
  }
14561
- function setAsideProposed(plan, proposals, baseSnapshotId) {
14562
- const carrying = carryingProposals(proposals, baseSnapshotId);
14563
- const carriedBy = (entry) => carrying.find(
14564
- (proposal) => proposal.changes.some(
14565
- (change) => change.path === entry.path && change.op === entry.action && (entry.action === "delete" || change.sha256 === entry.sha256)
14566
- )
15565
+ async function ask(question, streams) {
15566
+ const reader = createInterface2({
15567
+ input: streams.input,
15568
+ output: streams.output
15569
+ });
15570
+ try {
15571
+ return (await reader.question(question)).trim();
15572
+ } finally {
15573
+ reader.close();
15574
+ }
15575
+ }
15576
+ async function chooseProject(projects, streams = { input: process.stdin, output: process.stderr }) {
15577
+ streams.output.write(
15578
+ `This repository is connected to ${projects.length} projects:
15579
+ `
14567
15580
  );
14568
- const setAside = /* @__PURE__ */ new Set();
14569
- const alreadyProposed = [];
14570
- for (const entry of plan.entries) {
14571
- const proposal = carriedBy(entry);
14572
- if (proposal === void 0) {
14573
- continue;
14574
- }
14575
- setAside.add(entry.path);
14576
- alreadyProposed.push({
14577
- path: entry.path,
14578
- action: entry.action,
14579
- proposal: {
14580
- snapshotId: proposal.snapshotId,
14581
- version: proposal.version,
14582
- pullRequest: proposal.pullRequest
14583
- }
14584
- });
15581
+ for (const [index, project] of projects.entries()) {
15582
+ streams.output.write(` [${index + 1}] ${project.label}
15583
+ `);
14585
15584
  }
14586
- return {
14587
- plan: {
14588
- entries: plan.entries.filter((entry) => !setAside.has(entry.path)),
14589
- unchanged: plan.unchanged
14590
- },
14591
- alreadyProposed
14592
- };
15585
+ const answer = await ask(`Which one? [1-${projects.length}] `, streams);
15586
+ const number = /^\d+$/.test(answer) ? Number(answer) : 0;
15587
+ return number >= 1 && number <= projects.length ? number - 1 : null;
15588
+ }
15589
+ async function confirm(question, streams = { input: process.stdin, output: process.stderr }) {
15590
+ return /^y(es)?$/i.test(await ask(`${question} [y/N] `, streams));
14593
15591
  }
14594
15592
 
14595
15593
  // src/lib/instructions/resolve-project.ts
@@ -14685,8 +15683,8 @@ async function resolveProjectFromCheckout(input) {
14685
15683
  }
14686
15684
 
14687
15685
  // src/lib/instructions/self-update.ts
14688
- import { createHash as createHash13 } from "crypto";
14689
- import { mkdir as mkdir9, readFile as readFile8, realpath as realpath5, writeFile as writeFile5 } from "fs/promises";
15686
+ import { createHash as createHash14 } from "crypto";
15687
+ import { mkdir as mkdir10, readFile as readFile9, realpath as realpath5, writeFile as writeFile6 } from "fs/promises";
14690
15688
  import path26 from "path";
14691
15689
 
14692
15690
  // src/lib/instructions/npm-tarball.ts
@@ -14830,7 +15828,7 @@ async function isKeptCopy(script, copy) {
14830
15828
  }
14831
15829
  async function readState2(file) {
14832
15830
  try {
14833
- const parsed = JSON.parse(await readFile8(file, "utf8"));
15831
+ const parsed = JSON.parse(await readFile9(file, "utf8"));
14834
15832
  return typeof parsed.checkedAt === "number" && Number.isFinite(parsed.checkedAt) && typeof parsed.tarball === "string" ? { checkedAt: parsed.checkedAt, tarball: parsed.tarball } : null;
14835
15833
  } catch {
14836
15834
  return null;
@@ -14838,8 +15836,8 @@ async function readState2(file) {
14838
15836
  }
14839
15837
  async function writeState(file, state) {
14840
15838
  try {
14841
- await mkdir9(path26.dirname(file), { recursive: true });
14842
- await writeFile5(file, `${JSON.stringify(state)}
15839
+ await mkdir10(path26.dirname(file), { recursive: true });
15840
+ await writeFile6(file, `${JSON.stringify(state)}
14843
15841
  `);
14844
15842
  return true;
14845
15843
  } catch {
@@ -14957,7 +15955,7 @@ async function attempt({
14957
15955
  signal,
14958
15956
  fetchImpl
14959
15957
  );
14960
- const digest = `sha512-${createHash13("sha512").update(tarball).digest("base64")}`;
15958
+ const digest = `sha512-${createHash14("sha512").update(tarball).digest("base64")}`;
14961
15959
  if (digest !== served.integrity) {
14962
15960
  throw new SelfUpdateFailure(
14963
15961
  "the download does not match the integrity hash the deployment published"
@@ -15081,7 +16079,7 @@ var PROJECT_HELP = "Project ID (default: the project this checkout's repository
15081
16079
  var BASE_URL_HELP = "Deployment to talk to, such as https://example.com (default: the one you are signed in to)";
15082
16080
  var REMOTE_HELP = "Use this remote of the checkout (default: whichever fetches from the project's repository)";
15083
16081
  function buildInstructionsCommand() {
15084
- const instructions = new Command12("instructions").description(
16082
+ const instructions = new Command13("instructions").description(
15085
16083
  "Keep a checkout current with a project's published coding instructions"
15086
16084
  );
15087
16085
  instructions.command("check").description(
@@ -15417,6 +16415,70 @@ Waiting up to 5 minutes for the sign-in to finish.
15417
16415
  });
15418
16416
  }
15419
16417
  }
16418
+ async function ensureRepositoryTransportScope(opts, client) {
16419
+ let scopes;
16420
+ try {
16421
+ scopes = (await client.auth.whoami()).scopes;
16422
+ } catch (error) {
16423
+ throw fixedFailure(error);
16424
+ }
16425
+ if (scopes.includes(REPOSITORY_TRANSPORT_SCOPE)) {
16426
+ return;
16427
+ }
16428
+ if (opts.hook || runningInCi()) {
16429
+ throw new CliFailure(
16430
+ "This repository checkout needs the repositories:read capability. Run instructions init interactively to approve it, or use a CI API key with instructions:read and repositories:read from your organization's API keys settings.",
16431
+ 7
16432
+ );
16433
+ }
16434
+ const origin = deploymentOrigin(opts);
16435
+ try {
16436
+ await signInWithBrowser({
16437
+ baseUrl: opts.baseUrl ?? getBaseUrl() ?? origin,
16438
+ origin,
16439
+ explicit: opts.baseUrl !== void 0,
16440
+ project: opts.project,
16441
+ scopes: [...REQUESTED_SCOPES, REPOSITORY_TRANSPORT_SCOPE],
16442
+ signal: AbortSignal.timeout(SIGN_IN_TIMEOUT_MS),
16443
+ announce: (url) => {
16444
+ process.stderr.write(
16445
+ `Opening your browser to approve repository access at ${origin}.
16446
+ If it does not open, visit:
16447
+
16448
+ ${url}
16449
+
16450
+ Waiting up to 5 minutes for the sign-in to finish.
16451
+
16452
+ `
16453
+ );
16454
+ }
16455
+ });
16456
+ } catch {
16457
+ throw new CliFailure(
16458
+ "Repository access was not approved; the native checkout was left unchanged.",
16459
+ 7
16460
+ );
16461
+ }
16462
+ }
16463
+ async function directGitTransport(opts, direct) {
16464
+ if (direct.gitGateway?.version !== "v1") {
16465
+ throw new CliFailure(
16466
+ "This Fabric deployment does not support repository Git transport yet. Update Fabric, then run init again.",
16467
+ 7
16468
+ );
16469
+ }
16470
+ const origin = deploymentOrigin(opts);
16471
+ const base = new URL(
16472
+ `/api/v1/projects/${encodeURIComponent(opts.project)}/instructions/repository/git/${direct.generation}`,
16473
+ origin
16474
+ );
16475
+ const signal = opts.hook ? AbortSignal.timeout(hookDeadlineMs()) : void 0;
16476
+ const token = getOAuth(origin, opts.project) ? await projectAccessToken(origin, opts.project, signal) : getApiKey(origin, opts.project);
16477
+ if (!token) {
16478
+ throw new NotSignedInError(origin, opts.project);
16479
+ }
16480
+ return { url: base.toString(), authorization: `Bearer ${token}` };
16481
+ }
15420
16482
  function hasSignInFor(origin, project) {
15421
16483
  if (project === void 0) {
15422
16484
  return Boolean(getApiKey(origin));
@@ -15528,6 +16590,7 @@ var SourceGuard = class {
15528
16590
  };
15529
16591
  function sourceIdentity(published) {
15530
16592
  const repository = published.repository ?? null;
16593
+ const direct = directRepositoryRead(published);
15531
16594
  return JSON.stringify([
15532
16595
  published.sourceOfTruth,
15533
16596
  repository === null ? null : [
@@ -15537,9 +16600,61 @@ function sourceIdentity(published) {
15537
16600
  repository.ref,
15538
16601
  repository.rootPath,
15539
16602
  repository.generation
15540
- ]
16603
+ ],
16604
+ direct === null ? null : [direct.generation, direct.currentCommitSha]
15541
16605
  ]);
15542
16606
  }
16607
+ function directRepositoryRead(published) {
16608
+ const direct = published.direct;
16609
+ return published.sourceOfTruth === "REPOSITORY" && published.repository !== void 0 && published.repository !== null && direct?.availability === "READY" ? direct : null;
16610
+ }
16611
+ async function directRepositoryCheckoutStatus(input) {
16612
+ const commit = input.direct.currentCommitSha.slice(0, 7);
16613
+ const repository = repositoryName(input.repository);
16614
+ if (input.checkout.class !== "matching") {
16615
+ return {
16616
+ current: false,
16617
+ line: `Coding instructions are read directly from ${repository} at ${commit}. Open a matching native checkout before reading them locally.`
16618
+ };
16619
+ }
16620
+ const head = input.report.state?.head;
16621
+ if (head === null || head === void 0) {
16622
+ return {
16623
+ current: false,
16624
+ line: `Coding instructions are read directly from ${repository} at ${commit}; this checkout could not be compared to that commit.`
16625
+ };
16626
+ }
16627
+ const contains = await isAncestor(
16628
+ input.checkout.toplevel,
16629
+ input.direct.currentCommitSha,
16630
+ head,
16631
+ gitDeadline()
16632
+ );
16633
+ if (contains.kind === "ok" && contains.value) {
16634
+ return {
16635
+ current: true,
16636
+ line: `Coding instructions are read directly from ${repository} at ${commit}; this checkout already contains that commit.`
16637
+ };
16638
+ }
16639
+ if (contains.kind === "ok") {
16640
+ const behind = await isAncestor(
16641
+ input.checkout.toplevel,
16642
+ head,
16643
+ input.direct.currentCommitSha,
16644
+ gitDeadline()
16645
+ );
16646
+ if (behind.kind === "ok") {
16647
+ return {
16648
+ current: false,
16649
+ line: behind.value ? `Coding instructions are read directly from ${repository} at ${commit}; this checkout is behind. Run git pull --ff-only.` : `Coding instructions are read directly from ${repository} at ${commit}; this checkout has diverged. Inspect its Git history before choosing how to update it.`
16650
+ };
16651
+ }
16652
+ }
16653
+ return {
16654
+ current: false,
16655
+ line: `Coding instructions are read directly from ${repository} at ${commit}; fetch that commit with your normal Git remote before continuing.`
16656
+ };
16657
+ }
15543
16658
  async function readLockForProject2(destination, projectId) {
15544
16659
  const lock = await readLock(destination);
15545
16660
  if (lock !== null && lock.projectId !== projectId) {
@@ -15609,6 +16724,35 @@ async function runCheck(opts, format) {
15609
16724
  snapshot: published.snapshot,
15610
16725
  deadline: gitDeadline()
15611
16726
  });
16727
+ const direct = directRepositoryRead(published);
16728
+ if (direct !== null && published.repository) {
16729
+ const status = await directRepositoryCheckoutStatus({
16730
+ repository: published.repository,
16731
+ direct,
16732
+ checkout: guard.checkout ?? { class: "not-git" },
16733
+ report: checkout ?? await reportForClassification({
16734
+ classification: { class: "not-git" },
16735
+ repository: published.repository,
16736
+ snapshot: void 0,
16737
+ deadline: gitDeadline()
16738
+ })
16739
+ });
16740
+ if (format === "json") {
16741
+ printOutput(
16742
+ {
16743
+ projectId: opts.project,
16744
+ destination,
16745
+ direct,
16746
+ checkout: checkout?.json ?? null
16747
+ },
16748
+ { format: "json" }
16749
+ );
16750
+ } else if (!opts.hook || !status.current) {
16751
+ process.stdout.write(`${status.line}
16752
+ `);
16753
+ }
16754
+ return;
16755
+ }
15612
16756
  if (opts.hook && format !== "json" && checkout !== null && checkout.classification.class !== "not-git") {
15613
16757
  if (checkout.line !== null) {
15614
16758
  line(checkout.line);
@@ -15866,6 +17010,57 @@ async function syncOnce(opts) {
15866
17010
  const guard = opts.guard ?? new SourceGuard(destination, opts.remote);
15867
17011
  const hint = await lockDigestHint(existing, opts.project);
15868
17012
  let published = await fetchPublished(client, opts, hint, guard);
17013
+ const direct = directRepositoryRead(published);
17014
+ const directRepository = published.repository;
17015
+ if (direct !== null) {
17016
+ if (guard.checkout === null || guard.checkout.class !== "matching" || directRepository === null || directRepository === void 0) {
17017
+ throw new CliFailure(
17018
+ "This project reads coding instructions directly from its configured repository. Fabric does not copy them into this folder; open a matching native checkout or rerun init with --clone.",
17019
+ 7
17020
+ );
17021
+ }
17022
+ const report = await reportForClassification({
17023
+ classification: guard.checkout,
17024
+ repository: directRepository,
17025
+ snapshot: void 0,
17026
+ deadline: gitDeadline()
17027
+ });
17028
+ const status = await directRepositoryCheckoutStatus({
17029
+ repository: directRepository,
17030
+ direct,
17031
+ checkout: guard.checkout,
17032
+ report
17033
+ });
17034
+ const gitTransport = opts.hook && direct.gitGateway?.version === "v1" ? await directGitTransport(opts, direct) : void 0;
17035
+ const fastForward = opts.hook && guard.checkout.class === "matching" ? await runFastForward({
17036
+ classification: guard.checkout,
17037
+ repository: directRepository,
17038
+ snapshot: void 0,
17039
+ directCommitSha: direct.currentCommitSha,
17040
+ ...gitTransport === void 0 ? {} : { gitTransport },
17041
+ report: {
17042
+ ...report,
17043
+ reportKind: status.current ? "current" : "behind",
17044
+ line: status.current ? null : status.line
17045
+ },
17046
+ projectId: opts.project,
17047
+ deadline: gitDeadline(),
17048
+ optedOut: opts.fastForward === false,
17049
+ traceFile: traceFile(path28.dirname(getConfigPath()))
17050
+ }) : null;
17051
+ return {
17052
+ kind: "reported",
17053
+ destination: existing,
17054
+ published: false,
17055
+ version: null,
17056
+ checkout: {
17057
+ ...report.json,
17058
+ line: status.current ? null : status.line
17059
+ },
17060
+ current: status.current ? status.line : null,
17061
+ fastForward
17062
+ };
17063
+ }
15869
17064
  if (guard.checkout !== null && !downloadsIn(guard.checkout, !opts.hook)) {
15870
17065
  const refused = opts.hook ? null : refuseManualSync(guard.checkout, published.repository);
15871
17066
  if (refused !== null) {
@@ -16141,6 +17336,25 @@ async function runPush2(opts, format) {
16141
17336
  const note = opts.message !== void 0 ? splitMessage(opts.message) : void 0;
16142
17337
  const destination = destinationOf(opts);
16143
17338
  const root = await resolveExistingRoot(destination);
17339
+ const client = instructionsClient(
17340
+ { ...opts, hook: false },
17341
+ PUSH_TIMEOUT_MS2
17342
+ );
17343
+ const published = await fetchPublished(client, opts);
17344
+ const direct = directRepositoryRead(published);
17345
+ if (direct && published.repository) {
17346
+ await runNativeInstructionPush({
17347
+ ...opts,
17348
+ client,
17349
+ projectId: opts.project,
17350
+ root,
17351
+ repository: published.repository,
17352
+ direct,
17353
+ org: orgSlugFor(opts),
17354
+ format
17355
+ });
17356
+ return;
17357
+ }
16144
17358
  const lock = await readLockForProject2(root, opts.project);
16145
17359
  if (lock === null) {
16146
17360
  throw new CliFailure(
@@ -16148,11 +17362,6 @@ async function runPush2(opts, format) {
16148
17362
  7
16149
17363
  );
16150
17364
  }
16151
- const client = instructionsClient(
16152
- { ...opts, hook: false },
16153
- PUSH_TIMEOUT_MS2
16154
- );
16155
- const published = await fetchPublished(client, opts);
16156
17365
  if (!published.published || !published.snapshot) {
16157
17366
  throw new CliFailure(
16158
17367
  "this project has no published coding instructions to change yet; the first version is uploaded from the Coding Instructions tab",
@@ -16328,169 +17537,6 @@ async function runPush2(opts, format) {
16328
17537
  throw new CliFailure(verdict.text, 7);
16329
17538
  }
16330
17539
  }
16331
- function pushPullRequest(pullRequest, timedOut) {
16332
- const branch = pullRequest.branch ?? null;
16333
- return {
16334
- operationId: pullRequest.operationId,
16335
- state: pullRequest.state,
16336
- url: branch?.pullRequest?.url ?? pullRequest.url,
16337
- failure: pullRequest.failure,
16338
- timedOut,
16339
- branch,
16340
- append: pullRequest.append ?? null
16341
- };
16342
- }
16343
- var BLOCKED_REASONS = {
16344
- ATTRIBUTION_REJECTED: "the project's name or your display name looks like an address, a link or a credential, so the commit could not name you safely",
16345
- PERMISSION_REVOKED: "you no longer have permission to suggest changes to this project's repository",
16346
- CONFIGURATION_CHANGED: "the project's repository settings changed after you suggested this; push again",
16347
- TARGET_BRANCH_MISSING: "the repository branch the project syncs from no longer exists",
16348
- BASE_COMMIT_UNAVAILABLE: "the commit this suggestion was based on is no longer in the repository; sync and push again",
16349
- TREE_CONFLICT: "the repository changed the same files since your last sync; sync and push again",
16350
- AUTHENTICATION_FAILED: "the project's repository connection needs to be reconnected",
16351
- REPOSITORY_UNAVAILABLE: "the repository could not be reached",
16352
- BRANCH_WRITE_REFUSED: "the repository refused the new branch",
16353
- PR_CREATION_REFUSED: "the repository refused to open the pull request",
16354
- REMOTE_REF_CONFLICT: "a branch with the same name already exists in the repository",
16355
- CREATE_OUTCOME_UNKNOWN: "Fabric could not confirm whether the pull request was created",
16356
- VALIDATION_TIMEOUT: "the files' checks did not finish in time",
16357
- LIMITS_EXCEEDED: "the change is larger than Fabric can open as a pull request",
16358
- PROVIDER_RATE_LIMITED: "the repository's provider is limiting requests",
16359
- PROVIDER_TEMPORARY: "the repository's provider had a temporary problem",
16360
- LOOKUP_INCONCLUSIVE: "Fabric could not confirm the pull request's state",
16361
- // Member proposal branches (Fizzy #2738 spec §9). BRANCH_CONFLICT and
16362
- // SUPERSEDED_BY_LATER_CHANGE name their paths, in `blockedReason`.
16363
- BRANCH_MOVED: "your branch kept changing while Fabric was adding to it",
16364
- BRANCH_NAME_UNAVAILABLE: "Fabric could not reserve a branch name for your pull request; ask an owner to remove old proposal branches",
16365
- REPOSITORY_CHANGED: "the repository connection now points at another repository, so Fabric can no longer reach your pull request; close it in the repository, then stop tracking it in the Coding Instructions tab",
16366
- PUSH_OUTCOME_UNKNOWN: "Fabric could not confirm whether this change reached your branch, which may have been changed outside Fabric; check the branch, then try again or withdraw it",
16367
- BRANCH_MISSING: "your branch was deleted in the repository"
16368
- };
16369
- var FAILURE_PATHS_LISTED = 20;
16370
- function failurePaths(failure) {
16371
- const paths = failure.params?.paths;
16372
- const count = failure.params?.count;
16373
- if (typeof paths !== "string" || paths === "") {
16374
- return "some of its files";
16375
- }
16376
- return typeof count === "number" && count > FAILURE_PATHS_LISTED ? `${paths} and ${count - FAILURE_PATHS_LISTED} more` : paths;
16377
- }
16378
- function blockedReason(failure) {
16379
- switch (failure.code) {
16380
- case "BRANCH_CONFLICT":
16381
- return `files on your branch were changed outside Fabric (${failurePaths(failure)}), and Fabric does not overwrite them while the pull request is open; make this change on your branch in the repository, or wait until the pull request merges`;
16382
- case "SUPERSEDED_BY_LATER_CHANGE":
16383
- return `a newer change of yours already edits ${failurePaths(failure)}; use Try again to apply this one on top of it, or withdraw it`;
16384
- default:
16385
- return BLOCKED_REASONS[failure.code] ?? `it stopped with ${failure.code}`;
16386
- }
16387
- }
16388
- function pullRequestVerdict(pullRequest, { waited, openedHere }) {
16389
- const url = pullRequest.url ? `: ${pullRequest.url}` : "";
16390
- const branch = pullRequest.branch;
16391
- if (!waited) {
16392
- return {
16393
- text: "Follow its pull request in the project's Coding Instructions tab.",
16394
- fails: false
16395
- };
16396
- }
16397
- if (pullRequest.timedOut) {
16398
- return {
16399
- text: "The pull request is being opened; see the project's Coding Instructions tab.",
16400
- fails: false
16401
- };
16402
- }
16403
- switch (pullRequest.state) {
16404
- case "OPEN": {
16405
- if (!branch) {
16406
- return { text: `Pull request opened${url}`, fails: false };
16407
- }
16408
- if (branch.state === "MERGED") {
16409
- return {
16410
- text: `Your pull request was merged${url} while this change was being added to it; see the project's Coding Instructions tab for whether it carried this change.`,
16411
- fails: false
16412
- };
16413
- }
16414
- if (branch.state === "CLOSED") {
16415
- return {
16416
- text: `Your pull request was closed without merging${url} while this change was being added to it.`,
16417
- fails: false
16418
- };
16419
- }
16420
- if (branch.failure && (branch.state === "BLOCKED" || branchStoppedByFailure(branch))) {
16421
- const next = branch.failure.retryable ? "Fabric will try again on its own; see" : "See";
16422
- const what = branch.pullRequest ? `Your change is on your branch, but Fabric can no longer update its pull request${url}` : "Your change is on your branch, but Fabric could not open its pull request";
16423
- return {
16424
- text: `${what}: ${blockedReason(branch.failure)}. ${next} the project's Coding Instructions tab.`,
16425
- fails: true
16426
- };
16427
- }
16428
- if (branch.state === "CLOSE_REQUESTED") {
16429
- return {
16430
- text: "Your pull request is being closed, and this change leaves with it; push again once it has closed.",
16431
- fails: true
16432
- };
16433
- }
16434
- if (branch.state === "CANCELED") {
16435
- return {
16436
- text: "Your branch was closed before its pull request opened; push again.",
16437
- fails: true
16438
- };
16439
- }
16440
- if (branch.pullRequest?.state === "OPEN") {
16441
- return {
16442
- text: openedHere ? `Opened pull request ${branch.pullRequest.url}` : `Added to your pull request ${branch.pullRequest.url}`,
16443
- fails: false
16444
- };
16445
- }
16446
- return {
16447
- text: "Your change is on your branch and its pull request is being opened; see the project's Coding Instructions tab.",
16448
- fails: false
16449
- };
16450
- }
16451
- case "MERGED":
16452
- return {
16453
- text: `Its pull request was already merged${url}. Run \`${fabricCommand("instructions sync")}\` once the project has synced it.`,
16454
- fails: false
16455
- };
16456
- case "CLOSED":
16457
- return {
16458
- text: `Its pull request was closed without merging${url}.`,
16459
- fails: false
16460
- };
16461
- case "CANCELED":
16462
- if (pullRequest.failure?.code === "ALREADY_ON_BRANCH") {
16463
- return {
16464
- text: branch?.pullRequest ? `Already on your branch; nothing to add. Your pull request: ${branch.pullRequest.url}` : "Already on your branch; nothing to add.",
16465
- fails: false
16466
- };
16467
- }
16468
- return {
16469
- text: pullRequest.failure?.code === "VALIDATION_REJECTED" ? "The suggestion did not pass Fabric's checks, so no pull request was opened; see the project's Coding Instructions tab." : branch ? "The suggestion was withdrawn before it was added to your pull request." : "The suggestion was withdrawn before a pull request was opened.",
16470
- fails: true
16471
- };
16472
- case "CLOSE_REQUESTED":
16473
- return {
16474
- text: "An earlier attempt at this change was withdrawn and its pull request is being closed; push again once it has closed.",
16475
- fails: true
16476
- };
16477
- case "BLOCKED": {
16478
- const failure = pullRequest.failure ?? branch?.failure ?? null;
16479
- const reason = failure ? blockedReason(failure) : "it stopped with an unknown failure";
16480
- const next = failure?.retryable ? "Fabric will try again on its own; see" : "See";
16481
- const what = branch ? "Fabric could not add this change to your pull request" : "Fabric could not open the pull request";
16482
- return {
16483
- text: `${what}: ${reason}. ${next} the project's Coding Instructions tab.`,
16484
- fails: true
16485
- };
16486
- }
16487
- default:
16488
- return {
16489
- text: "The pull request is being opened; see the project's Coding Instructions tab.",
16490
- fails: false
16491
- };
16492
- }
16493
- }
16494
17540
  function asPushFailure(error) {
16495
17541
  const code = error.code;
16496
17542
  switch (code) {
@@ -16583,6 +17629,8 @@ function reportPush(outcome) {
16583
17629
  );
16584
17630
  }
16585
17631
  function proposalReference(proposal) {
17632
+ if (proposal.version === null)
17633
+ return proposal.pullRequestUrl ? `pull request ${proposal.pullRequestUrl}` : "a repository suggestion";
16586
17634
  if (proposal.pullRequestUrl) {
16587
17635
  return `version ${proposal.version}, pull request ${proposal.pullRequestUrl}`;
16588
17636
  }
@@ -16594,6 +17642,7 @@ function proposalReference(proposal) {
16594
17642
  function proposalReferences(alreadyProposed) {
16595
17643
  const byVersion = /* @__PURE__ */ new Map();
16596
17644
  for (const entry of alreadyProposed) {
17645
+ if (entry.proposal.version === null) continue;
16597
17646
  byVersion.set(
16598
17647
  entry.proposal.version,
16599
17648
  proposalReference({
@@ -16628,15 +17677,16 @@ async function isEmptyFolder(folder) {
16628
17677
  return error.code === "ENOENT";
16629
17678
  }
16630
17679
  }
16631
- async function cloneProjectRepository(opts, destination, repository) {
17680
+ async function cloneProjectRepository(opts, destination, repository, transport) {
16632
17681
  if (repository === null) {
16633
17682
  throw outcomeFailure("no-clone-url", { repo: "its repository" });
16634
17683
  }
16635
17684
  const repo = repositoryName(repository);
16636
- const url = cloneUrlFor(repository);
16637
- if (url === null) {
17685
+ const providerUrl = cloneUrlFor(repository);
17686
+ if (providerUrl === null) {
16638
17687
  throw outcomeFailure("no-clone-url", { repo });
16639
17688
  }
17689
+ const url = transport?.url ?? providerUrl;
16640
17690
  const ref = sanitizeDisplayText(repository.ref, 255);
16641
17691
  const wanted = opts.clone !== void 0 || canPrompt() && await confirm(`Clone ${repo} (${ref}) into this folder?`);
16642
17692
  if (!wanted) {
@@ -16648,14 +17698,15 @@ async function cloneProjectRepository(opts, destination, repository) {
16648
17698
  root,
16649
17699
  url,
16650
17700
  repository.ref,
16651
- Date.now() + CLONE_TIMEOUT_MS
17701
+ Date.now() + CLONE_TIMEOUT_MS,
17702
+ transport === null ? {} : { httpAuthorization: transport }
16652
17703
  );
16653
17704
  if (result.kind !== "cloned") {
16654
17705
  throw outcomeFailure("clone-failed", {
16655
17706
  repo,
16656
17707
  ref: repository.ref,
16657
17708
  host: repository.host,
16658
- url: shellQuote(url),
17709
+ url: shellQuote(providerUrl),
16659
17710
  reason: result.kind === "failed" && result.reason !== "not-empty" ? result.reason : "other",
16660
17711
  login: providerLoginCommand(
16661
17712
  repository.provider,
@@ -16663,6 +17714,20 @@ async function cloneProjectRepository(opts, destination, repository) {
16663
17714
  )
16664
17715
  });
16665
17716
  }
17717
+ if (transport !== null) {
17718
+ const restored = await setRemoteUrl(
17719
+ root,
17720
+ "origin",
17721
+ providerUrl,
17722
+ Date.now() + CLONE_TIMEOUT_MS
17723
+ );
17724
+ if (restored.kind !== "ok") {
17725
+ throw new CliFailure(
17726
+ "Fabric cloned the repository but could not restore its provider remote. Remove this new checkout and run init again.",
17727
+ 7
17728
+ );
17729
+ }
17730
+ }
16666
17731
  return classifyCheckout({
16667
17732
  destination: root,
16668
17733
  repository,
@@ -16697,6 +17762,8 @@ async function runInit(opts, format) {
16697
17762
  freshCloneDestination
16698
17763
  );
16699
17764
  const published = await fetchPublished(client, opts, void 0, guard);
17765
+ const direct = directRepositoryRead(published);
17766
+ let repositoryTransportApproved = false;
16700
17767
  const repository = published.repository ?? null;
16701
17768
  let checkout = guard.checkout;
16702
17769
  if (cloneFolder !== void 0 && checkout === null) {
@@ -16719,10 +17786,17 @@ async function runInit(opts, format) {
16719
17786
  throw planned.failure;
16720
17787
  }
16721
17788
  if (planned.kind === "empty-folder") {
17789
+ let transport = null;
17790
+ if (direct?.gitGateway?.version === "v1") {
17791
+ await ensureRepositoryTransportScope(opts, client);
17792
+ repositoryTransportApproved = true;
17793
+ transport = await directGitTransport(opts, direct);
17794
+ }
16722
17795
  checkout = await cloneProjectRepository(
16723
17796
  opts,
16724
17797
  destination,
16725
- repository
17798
+ repository,
17799
+ transport
16726
17800
  );
16727
17801
  cloned = true;
16728
17802
  if (cloneFolder !== void 0 && checkout.class === "unmapped") {
@@ -16819,6 +17893,10 @@ async function runInit(opts, format) {
16819
17893
  );
16820
17894
  }
16821
17895
  }
17896
+ if (direct?.gitGateway?.version === "v1" && matching !== null && !repositoryTransportApproved) {
17897
+ await ensureRepositoryTransportScope(opts, client);
17898
+ repositoryTransportApproved = true;
17899
+ }
16822
17900
  let outcome = null;
16823
17901
  if (checkout === null && published.published) {
16824
17902
  const synced = await syncOnce({ ...opts, hook: false, guard });
@@ -16943,7 +18021,22 @@ async function runInit(opts, format) {
16943
18021
  line("Removed the Stop hook for lesson capture.");
16944
18022
  }
16945
18023
  }
16946
- if (matching !== null && repository !== null) {
18024
+ if (matching !== null && repository !== null && direct !== null) {
18025
+ const report = await reportForClassification({
18026
+ classification: matching,
18027
+ repository,
18028
+ snapshot: void 0,
18029
+ deadline: gitDeadline()
18030
+ });
18031
+ line(
18032
+ (await directRepositoryCheckoutStatus({
18033
+ repository,
18034
+ direct,
18035
+ checkout: matching,
18036
+ report
18037
+ })).line
18038
+ );
18039
+ } else if (matching !== null && repository !== null) {
16947
18040
  if (published.snapshot?.source?.kind !== "REPOSITORY") {
16948
18041
  line(nothingPublishedLine(repository));
16949
18042
  }
@@ -17042,9 +18135,9 @@ async function runLessonPromptCommand(opts) {
17042
18135
  }
17043
18136
 
17044
18137
  // src/commands/mcp/index.ts
17045
- import { Command as Command13 } from "commander";
18138
+ import { Command as Command14 } from "commander";
17046
18139
  function buildMcpCommand() {
17047
- const mcp = new Command13("mcp").description(
18140
+ const mcp = new Command14("mcp").description(
17048
18141
  "Manage MCP servers and configurations"
17049
18142
  );
17050
18143
  mcp.command("servers").description("List accessible MCP servers (system + custom)").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option(
@@ -17082,7 +18175,7 @@ function buildMcpCommand() {
17082
18175
  });
17083
18176
  }
17084
18177
  );
17085
- const configs = new Command13("configs").description(
18178
+ const configs = new Command14("configs").description(
17086
18179
  "Manage MCP configurations"
17087
18180
  );
17088
18181
  configs.command("list", { isDefault: true }).description("List your MCP configurations").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option(
@@ -17186,9 +18279,9 @@ function buildMcpCommand() {
17186
18279
  }
17187
18280
 
17188
18281
  // src/commands/orgs/index.ts
17189
- import { Command as Command14 } from "commander";
18282
+ import { Command as Command15 } from "commander";
17190
18283
  function buildOrgsCommand() {
17191
- const orgs = new Command14("orgs").description("Manage organizations");
18284
+ const orgs = new Command15("orgs").description("Manage organizations");
17192
18285
  orgs.command("list").description("List organizations you belong to").option(
17193
18286
  "--format <format>",
17194
18287
  "Output format: table|json|yaml|csv",
@@ -17244,9 +18337,9 @@ function buildOrgsCommand() {
17244
18337
  }
17245
18338
 
17246
18339
  // src/commands/projects/index.ts
17247
- import { Command as Command15 } from "commander";
18340
+ import { Command as Command16 } from "commander";
17248
18341
  function buildProjectsCommand() {
17249
- const projects = new Command15("projects").description("Manage projects");
18342
+ const projects = new Command16("projects").description("Manage projects");
17250
18343
  projects.command("list").description("List projects").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option(
17251
18344
  "--status <status>",
17252
18345
  "Filter by status: ACTIVE|DRAFT|COMPLETED|ARCHIVED"
@@ -17378,9 +18471,9 @@ function buildProjectsCommand() {
17378
18471
  }
17379
18472
 
17380
18473
  // src/commands/prompts/index.ts
17381
- import { Command as Command16 } from "commander";
18474
+ import { Command as Command17 } from "commander";
17382
18475
  function buildPromptsCommand() {
17383
- const prompts = new Command16("prompts").description(
18476
+ const prompts = new Command17("prompts").description(
17384
18477
  "Manage prompt templates"
17385
18478
  );
17386
18479
  prompts.command("list").description("List prompt templates").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--search <query>", "Search by name").option("--category <cat>", "Filter by category").option("--limit <n>", "Max results", "50").option(
@@ -17532,12 +18625,12 @@ ${content}
17532
18625
  }
17533
18626
 
17534
18627
  // src/commands/reports/index.ts
17535
- import { Command as Command17 } from "commander";
18628
+ import { Command as Command18 } from "commander";
17536
18629
  function buildReportsCommand() {
17537
- const reports = new Command17("reports").description(
18630
+ const reports = new Command18("reports").description(
17538
18631
  "Manage report templates and executions"
17539
18632
  );
17540
- const templates = new Command17("templates").description(
18633
+ const templates = new Command18("templates").description(
17541
18634
  "Manage report templates"
17542
18635
  );
17543
18636
  templates.command("list", { isDefault: true }).description("List report templates").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--search <query>", "Search by name").option("--category <cat>", "Filter by category").option("--limit <n>", "Max results (default 20)", "20").option(
@@ -17610,7 +18703,7 @@ function buildReportsCommand() {
17610
18703
  }
17611
18704
  );
17612
18705
  reports.addCommand(templates);
17613
- const executions = new Command17("executions").description(
18706
+ const executions = new Command18("executions").description(
17614
18707
  "View report executions"
17615
18708
  );
17616
18709
  executions.command("list", { isDefault: true }).description("List report executions").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--limit <n>", "Max results (default 20)", "20").option(
@@ -17654,9 +18747,9 @@ function buildReportsCommand() {
17654
18747
  }
17655
18748
 
17656
18749
  // src/commands/skills/index.ts
17657
- import { Command as Command18 } from "commander";
18750
+ import { Command as Command19 } from "commander";
17658
18751
  function buildSkillsCommand() {
17659
- const skills = new Command18("skills").description(
18752
+ const skills = new Command19("skills").description(
17660
18753
  "Browse skill definitions"
17661
18754
  );
17662
18755
  skills.command("list").description("List skills").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--search <query>", "Search by name").option("--category <cat>", "Filter by category").option("--limit <n>", "Max results", "50").option(
@@ -17722,9 +18815,9 @@ function buildSkillsCommand() {
17722
18815
  }
17723
18816
 
17724
18817
  // src/commands/workflows/index.ts
17725
- import { Command as Command19 } from "commander";
18818
+ import { Command as Command20 } from "commander";
17726
18819
  function buildWorkflowsCommand() {
17727
- const workflows = new Command19("workflows").description("Manage workflows");
18820
+ const workflows = new Command20("workflows").description("Manage workflows");
17728
18821
  workflows.command("list").description("List workflows").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--status <status>", "Filter: DRAFT|ACTIVE|PAUSED|ARCHIVED").option("--search <query>", "Search by name").option("--limit <n>", "Max results", "20").option(
17729
18822
  "--format <format>",
17730
18823
  "Output format: table|json|yaml|csv",
@@ -17851,9 +18944,9 @@ function buildWorkflowsCommand() {
17851
18944
  }
17852
18945
 
17853
18946
  // src/commands/workspaces/index.ts
17854
- import { Command as Command20 } from "commander";
18947
+ import { Command as Command21 } from "commander";
17855
18948
  function buildWorkspacesCommand() {
17856
- const workspaces = new Command20("workspaces").description(
18949
+ const workspaces = new Command21("workspaces").description(
17857
18950
  "Manage workspaces"
17858
18951
  );
17859
18952
  workspaces.command("list").description("List workspaces").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--search <query>", "Search by name").option("--status <status>", "Filter: ACTIVE|ARCHIVED").option("--limit <n>", "Max results (default 20)", "20").option(
@@ -17928,13 +19021,13 @@ function buildWorkspacesCommand() {
17928
19021
  }
17929
19022
 
17930
19023
  // src/bin/fabric.ts
17931
- var program = new Command21();
19024
+ var program = new Command22();
17932
19025
  program.name("fabric").description("Fabric AI platform CLI").version(package_default.version).option(
17933
19026
  "--format <format>",
17934
19027
  "Output format: table|json|yaml|csv",
17935
19028
  process.env.FABRIC_FORMAT ?? "table"
17936
19029
  );
17937
- var auth = new Command21("auth").description(
19030
+ var auth = new Command22("auth").description(
17938
19031
  "Authenticate and manage API keys"
17939
19032
  );
17940
19033
  auth.addCommand(buildLoginCommand());
@@ -17942,6 +19035,7 @@ auth.addCommand(buildLogoutCommand());
17942
19035
  auth.addCommand(buildWhoamiCommand());
17943
19036
  auth.addCommand(buildKeysCommand());
17944
19037
  program.addCommand(auth);
19038
+ program.addCommand(buildConnectCommand());
17945
19039
  program.addCommand(buildCtxCommand());
17946
19040
  program.addCommand(buildOrgsCommand());
17947
19041
  program.addCommand(buildProjectsCommand());
@@ -17958,7 +19052,7 @@ program.addCommand(buildWorkspacesCommand());
17958
19052
  program.addCommand(buildChatsCommand());
17959
19053
  program.addCommand(buildReportsCommand());
17960
19054
  program.addCommand(buildCompletionCommand());
17961
- var config = new Command21("config").description("Show CLI configuration");
19055
+ var config = new Command22("config").description("Show CLI configuration");
17962
19056
  config.command("path").description("Print the path to the config file").action(() => {
17963
19057
  process.stdout.write(`${getConfigPath()}
17964
19058
  `);