@fabricorg/cli 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fabric.js CHANGED
@@ -7,7 +7,7 @@ import { Command as Command25 } from "commander";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@fabricorg/cli",
10
- version: "0.9.0",
10
+ version: "0.9.1",
11
11
  description: "CLI for the Fabric AI platform",
12
12
  keywords: [
13
13
  "fabric",
@@ -1171,6 +1171,20 @@ var ContextsResource = class {
1171
1171
  throw asConflictError(error);
1172
1172
  }
1173
1173
  }
1174
+ /**
1175
+ * Which of up to 500 paths hold no synced file in the project, for example
1176
+ * after a Living Memory reset. Writes nothing.
1177
+ *
1178
+ * Requires a key with `projects:write`, and the key's creator must still
1179
+ * hold permission to read context sources on that project.
1180
+ */
1181
+ findMissingSyncedFiles(projectId, sourcePaths, options = {}) {
1182
+ const query2 = options.org ? `?org=${encodeURIComponent(options.org)}` : "";
1183
+ return this.http.post(
1184
+ `/projects/${encodeURIComponent(projectId)}/contexts/synced-files/missing${query2}`,
1185
+ { sourcePaths }
1186
+ );
1187
+ }
1174
1188
  /**
1175
1189
  * Reset the project's Living Memory: delete EVERY file synced into its
1176
1190
  * Context at once, with their search-index entries queued for removal.
@@ -6753,6 +6767,9 @@ function asCliFailure(error) {
6753
6767
  }
6754
6768
 
6755
6769
  // src/lib/missing-scope.ts
6770
+ function signInCanGrant(scope) {
6771
+ return REQUESTED_SCOPES.some((requested) => requested === scope);
6772
+ }
6756
6773
  function readProperty(error, name) {
6757
6774
  return typeof error === "object" && error !== null && name in error ? Reflect.get(error, name) : void 0;
6758
6775
  }
@@ -6776,7 +6793,10 @@ function missingScopeOf(error) {
6776
6793
  // src/lib/context-sync/failure.ts
6777
6794
  function missingScopeSentence(scope) {
6778
6795
  const named = scope ?? "projects:write";
6779
- return `This API key is missing the ${named} scope. Create an API key with the ${named} scope and run again, or sign in with: ${fabricCommand("auth login")}`;
6796
+ if (signInCanGrant(named)) {
6797
+ return `This credential is missing the ${named} scope. Create an API key with the ${named} scope and run again, or sign in with: ${fabricCommand("auth login")}`;
6798
+ }
6799
+ return `This credential is missing the ${named} scope, which a browser sign-in does not carry. Create an API key with the ${named} scope and sign in with it: ${fabricCommand("auth login")} --key <api-key>`;
6780
6800
  }
6781
6801
  function contextCliFailure(error) {
6782
6802
  if (error instanceof CliFailure) {
@@ -7462,6 +7482,32 @@ function lockProblem(value) {
7462
7482
  return null;
7463
7483
  }
7464
7484
 
7485
+ // src/lib/context-sync/missing-on-server.ts
7486
+ var PATHS_PER_REQUEST = 500;
7487
+ var NOTHING_MISSING = {
7488
+ paths: [],
7489
+ incomplete: false
7490
+ };
7491
+ async function findMissingOnServer(input) {
7492
+ const confirmed = input.unchanged.filter(
7493
+ (sourcePath) => input.lock?.files[sourcePath]?.state !== "duplicate"
7494
+ );
7495
+ const paths = [];
7496
+ try {
7497
+ for (let i = 0; i < confirmed.length; i += PATHS_PER_REQUEST) {
7498
+ const answer = await input.client.contexts.findMissingSyncedFiles(
7499
+ input.projectId,
7500
+ confirmed.slice(i, i + PATHS_PER_REQUEST),
7501
+ { org: input.org }
7502
+ );
7503
+ paths.push(...answer.missing);
7504
+ }
7505
+ } catch {
7506
+ return { paths, incomplete: true };
7507
+ }
7508
+ return { paths, incomplete: false };
7509
+ }
7510
+
7465
7511
  // src/lib/context-sync/classify.ts
7466
7512
  import { createHash as createHash3 } from "crypto";
7467
7513
  import path4 from "path";
@@ -8481,12 +8527,12 @@ function managedSkips(results) {
8481
8527
  detail: r.message
8482
8528
  }));
8483
8529
  }
8484
- function countOutcomes(plan, results) {
8530
+ function countOutcomes(plan, results, missingOnServer) {
8485
8531
  const count = (status2) => results.filter((r) => r.status === status2).length;
8486
8532
  return {
8487
8533
  created: count("created"),
8488
8534
  updated: count("updated"),
8489
- unchanged: count("unchanged") + plan.unchangedLocal.length,
8535
+ unchanged: count("unchanged") + plan.unchangedLocal.length - missingOnServer.length,
8490
8536
  duplicate: count("duplicate"),
8491
8537
  moved: count("moved"),
8492
8538
  moveUnsupported: count("move-unsupported"),
@@ -8609,6 +8655,7 @@ function group(lines2, label, items) {
8609
8655
  }
8610
8656
  function formatPushReport(input) {
8611
8657
  const { plan, results } = input;
8658
+ const missing = input.missingOnServer ?? NOTHING_MISSING;
8612
8659
  const prune2 = Boolean(input.prune);
8613
8660
  const of = (status2) => results.filter(
8614
8661
  (r) => r.status === status2 && !isMoveFallback(r)
@@ -8693,7 +8740,18 @@ function formatPushReport(input) {
8693
8740
  "skipped",
8694
8741
  [...plan.skipped, ...managedSkips(results)].map(skippedLine)
8695
8742
  );
8696
- const unchanged = countOutcomes(plan, results).unchanged;
8743
+ group(lines2, "missing on the server", missing.paths);
8744
+ if (missing.paths.length > 0) {
8745
+ lines2.push(
8746
+ `These files are in this folder's lock but no longer on the server, so nothing was sent for them: a Living Memory reset or a delete removed them. To send them again, run: ${fabricCommand(`context push --init <dir> --project ${input.projectId}`)}`
8747
+ );
8748
+ }
8749
+ if (missing.incomplete) {
8750
+ lines2.push(
8751
+ "Could not check whether the server still holds the files listed as unchanged, so some may be missing. Run this push again to check."
8752
+ );
8753
+ }
8754
+ const unchanged = countOutcomes(plan, results, missing.paths).unchanged;
8697
8755
  if (unchanged > 0) {
8698
8756
  lines2.push(`unchanged (${unchanged})`);
8699
8757
  }
@@ -8764,7 +8822,13 @@ function contextPushJson(input) {
8764
8822
  skipped: input.plan.skipped
8765
8823
  },
8766
8824
  results: input.results,
8767
- counts: countOutcomes(input.plan, input.results)
8825
+ missingOnServer: input.missingOnServer.paths,
8826
+ missingCheckIncomplete: input.missingOnServer.incomplete,
8827
+ counts: countOutcomes(
8828
+ input.plan,
8829
+ input.results,
8830
+ input.missingOnServer.paths
8831
+ )
8768
8832
  };
8769
8833
  }
8770
8834
 
@@ -9636,6 +9700,13 @@ async function runPush(dir, opts, format) {
9636
9700
  if (next !== null) {
9637
9701
  await writeContextLock(root, next);
9638
9702
  }
9703
+ const missingOnServer = pushed.stoppedBy === null ? await findMissingOnServer({
9704
+ client,
9705
+ projectId: opts.project,
9706
+ org: opts.org,
9707
+ lock,
9708
+ unchanged: plan.unchangedLocal
9709
+ }) : NOTHING_MISSING;
9639
9710
  if (format === "json") {
9640
9711
  printOutput(
9641
9712
  jsonOutcome({
@@ -9643,7 +9714,8 @@ async function runPush(dir, opts, format) {
9643
9714
  opts,
9644
9715
  plan,
9645
9716
  results: pushed.results,
9646
- lockWritten: next !== null
9717
+ lockWritten: next !== null,
9718
+ missingOnServer
9647
9719
  }),
9648
9720
  { format: "json" }
9649
9721
  );
@@ -9655,7 +9727,8 @@ async function runPush(dir, opts, format) {
9655
9727
  plan,
9656
9728
  results: pushed.results,
9657
9729
  prune: Boolean(opts.prune),
9658
- pruneTargets: pushed.pruneTargets
9730
+ pruneTargets: pushed.pruneTargets,
9731
+ missingOnServer
9659
9732
  })
9660
9733
  );
9661
9734
  }
@@ -9678,7 +9751,8 @@ function jsonOutcome(input) {
9678
9751
  force: Boolean(input.opts.force),
9679
9752
  prune: Boolean(input.opts.prune),
9680
9753
  plan: input.plan,
9681
- results: input.results
9754
+ results: input.results,
9755
+ missingOnServer: input.missingOnServer ?? NOTHING_MISSING
9682
9756
  });
9683
9757
  }
9684
9758
  function pruneSummary(results, targets) {
@@ -11623,7 +11697,7 @@ var LINES = {
11623
11697
  "lock-of-other-project": () => "This folder was synced from a different project, so nothing was changed. Use another --dest, or delete .fabric/instructions.lock to start over.",
11624
11698
  "sign-in-expired": ({ origin, project }) => `Your sign-in to ${origin} has expired. Run: ${loginCommand(origin, project)}`,
11625
11699
  forbidden: () => "You do not have access to this project's coding instructions. Ask a project maintainer for access.",
11626
- "missing-scope": ({ scope, origin }) => `This credential is missing ${scope === null ? "a permission this needs" : `the ${scope} permission`}. Create a key that carries it, or run: ${loginCommand(origin)}`,
11700
+ "missing-scope": ({ scope, origin }) => scope !== null && !signInCanGrant(scope) ? `This credential is missing the ${scope} permission, which a browser sign-in does not carry. Create a key that carries it and sign in with it: ${loginCommand(origin)} --key <api-key>` : `This credential is missing ${scope === null ? "a permission this needs" : `the ${scope} permission`}. Create a key that carries it, or run: ${loginCommand(origin)}`,
11627
11701
  "proposal-limit-proposer": () => "You already have five active coding-instructions proposals on this project. Wait for one to be reviewed, or withdraw one. Nothing was sent.",
11628
11702
  "proposal-limit-project": () => "This project already has as many active coding-instructions proposals as it allows. Wait for some to be reviewed. Nothing was sent.",
11629
11703
  "note-rejected": () => "The note was refused: it is too long or holds text that is not allowed. Change --message and push again; nothing was sent.",
@@ -19468,6 +19542,13 @@ var REBASE_STATE_PATH = `${LOCK_DIRECTORY}/rebase.json`;
19468
19542
  var REBASE_STATE_VERSION = 1;
19469
19543
  var VERSION_FILES_CHUNK = 200;
19470
19544
  var DEFAULT_MODE2 = 33188;
19545
+ var BaseVersionRemovedError = class extends Error {
19546
+ constructor(version) {
19547
+ super(`Version ${version} is no longer kept.`);
19548
+ this.version = version;
19549
+ this.name = "BaseVersionRemovedError";
19550
+ }
19551
+ };
19471
19552
  function sha256Of2(bytes) {
19472
19553
  return createHash14("sha256").update(bytes).digest("hex");
19473
19554
  }
@@ -19494,7 +19575,14 @@ async function readBaseRecord(input) {
19494
19575
  input.lock.snapshotId,
19495
19576
  chunk,
19496
19577
  { org: input.org }
19497
- );
19578
+ ).catch((error) => {
19579
+ if (error.status === 404) {
19580
+ throw new BaseVersionRemovedError(
19581
+ input.lock.snapshotVersion
19582
+ );
19583
+ }
19584
+ throw error;
19585
+ });
19498
19586
  if (response.snapshotId !== input.lock.snapshotId) {
19499
19587
  throw new Error(
19500
19588
  "The server answered for a different version than the one this checkout last synced. Nothing was sent."
@@ -24978,7 +25066,12 @@ async function runCheck(opts, format) {
24978
25066
  published
24979
25067
  ));
24980
25068
  }
24981
- const drift = opts.verify && lock !== null ? await findLedgerDrift({ root, lock }) : [];
25069
+ const ledgerDrift = opts.verify && lock !== null ? await findLedgerDrift({ root, lock }) : [];
25070
+ const isOverridden = overrideMatcher(
25071
+ ledgerDrift.length === 0 ? NO_OVERRIDES : await readLocalOverrides(root)
25072
+ );
25073
+ const drift = ledgerDrift.filter((entry) => !isOverridden(entry.path));
25074
+ const overriddenDrift = ledgerDrift.filter((entry) => isOverridden(entry.path)).map((entry) => entry.path);
24982
25075
  if (format === "json") {
24983
25076
  printOutput(
24984
25077
  {
@@ -24988,6 +25081,7 @@ async function runCheck(opts, format) {
24988
25081
  localDigest: lock?.digest ?? null,
24989
25082
  verified: Boolean(opts.verify),
24990
25083
  drifted: drift.map(describeLedgerDrift),
25084
+ localOverrides: overriddenDrift,
24991
25085
  // Spec §6.4: the edits an earlier sync saw and kept.
24992
25086
  keptEdited: drift.filter((entry) => entry.reason === "edited" && entry.kept).map((entry) => entry.path),
24993
25087
  ...published,
@@ -25009,7 +25103,7 @@ async function runCheck(opts, format) {
25009
25103
  );
25010
25104
  }
25011
25105
  if (opts.verify) {
25012
- reportDrift(opts, drift);
25106
+ reportDrift(opts, drift, overriddenDrift);
25013
25107
  }
25014
25108
  if (checkout?.line) {
25015
25109
  line(checkout.line);
@@ -25071,11 +25165,15 @@ function describeRepositorySource(source, repository) {
25071
25165
  }
25072
25166
  return repository ? `${base} (${repository.path})` : base;
25073
25167
  }
25074
- function reportDrift(opts, drift) {
25168
+ function reportDrift(opts, drift, overridden) {
25075
25169
  if (drift.length === 0) {
25076
- line("Every file in this folder matches the lock.");
25170
+ line(
25171
+ overridden.length === 0 ? "Every file in this folder matches the lock." : "Every file in this folder matches the lock, apart from your local overrides:"
25172
+ );
25173
+ listPaths("local override", overridden);
25077
25174
  return;
25078
25175
  }
25176
+ listPaths("local override", overridden);
25079
25177
  line(`${drift.length} local file(s) no longer match the lock:`);
25080
25178
  listPaths("drifted", drift.map(describeLedgerDrift));
25081
25179
  const edits = drift.filter((entry) => entry.reason === "edited").length;
@@ -25889,6 +25987,12 @@ async function rebaseOntoPublished(input) {
25889
25987
  7
25890
25988
  );
25891
25989
  }
25990
+ if (error instanceof BaseVersionRemovedError) {
25991
+ throw new CliFailure(
25992
+ `Version ${error.version}, which this folder last synced, is no longer kept, so your edits cannot be merged automatically. Nothing was sent. Run \`${fabricCommand("instructions sync")}\`, which keeps your edits. A push then sends each edited file whole, so first check that those files still hold what was published since, then run \`${fabricCommand("instructions push")}\`.`,
25993
+ 7
25994
+ );
25995
+ }
25892
25996
  throw error;
25893
25997
  }
25894
25998
  }
@@ -25988,6 +26092,12 @@ function reportPush(outcome) {
25988
26092
  return;
25989
26093
  }
25990
26094
  line(pushVerdict(outcome));
26095
+ if (outcome.rebasedFromVersion !== null) {
26096
+ line(
26097
+ `Your lock was not changed: it still names version ${outcome.rebasedFromVersion}. \`${fabricCommand("instructions sync")}\` moves it to the published version and keeps your merged edits.`
26098
+ );
26099
+ return;
26100
+ }
25991
26101
  line(
25992
26102
  outcome.publish ? `Your lock was not changed: it still names version ${outcome.baseVersion}, which is what \`${fabricCommand("instructions sync")}\` compares against.` : `Your lock was not changed: it still names the published version, which is what \`${fabricCommand("instructions sync")}\` compares against.`
25993
26103
  );