@inerrata-corporation/errata 2.0.7-dev.2797 → 2.0.7-dev.2822

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.
Files changed (2) hide show
  1. package/errata.mjs +123 -12
  2. package/package.json +1 -1
package/errata.mjs CHANGED
@@ -79931,7 +79931,7 @@ function createWatchBreaker(deps) {
79931
79931
  }
79932
79932
 
79933
79933
  // src/engine.ts
79934
- var DAEMON_VERSION = true ? "2.0.7-dev.2797" : "2.0.0-alpha.0";
79934
+ var DAEMON_VERSION = true ? "2.0.7-dev.2822" : "2.0.0-alpha.0";
79935
79935
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
79936
79936
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
79937
79937
  var GIT_OP_MUTE_MS = 4e3;
@@ -82219,6 +82219,14 @@ B: ${b}`,
82219
82219
  const pending = existsSync25(paths.outbox) ? readdirSync10(paths.outbox).filter((f) => f.endsWith(".json")).length : 0;
82220
82220
  return { uploaded: 0, failed: 0, remaining: pending };
82221
82221
  }
82222
+ const onDisk = loadProfile(opts.workspaceRoot);
82223
+ if (!onDisk?.projectId && onDisk?.projectLinkHold) {
82224
+ console.log(
82225
+ "[errata] sync held \u2014 this repo is already on errata in another workspace; run `errata link` to join it or create your own project"
82226
+ );
82227
+ const pending = existsSync25(paths.outbox) ? readdirSync10(paths.outbox).filter((f) => f.endsWith(".json")).length : 0;
82228
+ return { uploaded: 0, failed: 0, remaining: pending };
82229
+ }
82222
82230
  try {
82223
82231
  const ignorePatterns = loadClaimIgnorePatterns(opts.workspaceRoot);
82224
82232
  const claimPayload = buildClaimIngest(store, profile, DAEMON_VERSION, 1, ignorePatterns);
@@ -83715,6 +83723,11 @@ init_profile2();
83715
83723
  import { execFile as execFile3 } from "node:child_process";
83716
83724
  import { promisify } from "node:util";
83717
83725
  var execFileAsync = promisify(execFile3);
83726
+ function isClaimedElsewhere(err2) {
83727
+ if (!err2 || typeof err2 !== "object") return false;
83728
+ const e = err2;
83729
+ return e.status === 409 && e.code === "claimed_elsewhere";
83730
+ }
83718
83731
  async function readRootCommit(root) {
83719
83732
  try {
83720
83733
  const { stdout } = await execFileAsync("git", ["rev-list", "--max-parents=0", "HEAD"], {
@@ -83753,9 +83766,21 @@ async function ensureProjectLink(root, profile, client, rootCommit = readRootCom
83753
83766
  profile.projectId = res.project.id;
83754
83767
  profile.projectLocator = locator;
83755
83768
  if (sha2) profile.rootCommitReportedFor = res.project.id;
83769
+ delete profile.projectLinkHold;
83756
83770
  saveProfile(root, profile);
83757
83771
  return { linked: true, projectId: res.project.id, created: res.created };
83758
83772
  } catch (err2) {
83773
+ if (isClaimedElsewhere(err2)) {
83774
+ if (profile.projectLinkHold?.locator !== locator) {
83775
+ profile.projectLinkHold = { reason: "claimed-elsewhere", locator, since: Date.now() };
83776
+ saveProfile(root, profile);
83777
+ }
83778
+ return {
83779
+ linked: false,
83780
+ reason: "held",
83781
+ detail: "this repo is already on errata in another workspace \u2014 run `errata link` to join it or create your own project"
83782
+ };
83783
+ }
83759
83784
  return {
83760
83785
  linked: false,
83761
83786
  reason: "error",
@@ -85034,16 +85059,28 @@ async function loginOAuthDevice(opts) {
85034
85059
  if (approvedInstallation !== opts.installationId) {
85035
85060
  throw new Error("approved installation does not match the terminal request");
85036
85061
  }
85037
- const resourceResponse = await fetchFn(device.resource, {
85062
+ return exchangeDeviceSession({
85063
+ fetchFn,
85064
+ resourceUrl: device.resource,
85065
+ deviceSession,
85066
+ installationId: approvedInstallation,
85067
+ clientId,
85068
+ scope
85069
+ });
85070
+ }
85071
+ async function exchangeDeviceSession(opts) {
85072
+ const fetchFn = opts.fetchFn ?? fetch;
85073
+ const scope = opts.scope ?? DEFAULT_OAUTH_SCOPE;
85074
+ const resourceResponse = await fetchFn(opts.resourceUrl, {
85038
85075
  method: "POST",
85039
85076
  headers: {
85040
- authorization: `Bearer ${deviceSession}`,
85077
+ authorization: `Bearer ${opts.deviceSession}`,
85041
85078
  "content-type": "application/json",
85042
85079
  accept: "application/json"
85043
85080
  },
85044
85081
  body: JSON.stringify({
85045
- installationId: approvedInstallation,
85046
- clientId,
85082
+ installationId: opts.installationId,
85083
+ clientId: opts.clientId,
85047
85084
  scopes: scope.split(/\s+/).filter((value) => !["openid", "profile", "email", "offline_access"].includes(value))
85048
85085
  })
85049
85086
  });
@@ -85052,12 +85089,12 @@ async function loginOAuthDevice(opts) {
85052
85089
  throw new Error(`device resource exchange failed: HTTP ${resourceResponse.status}`);
85053
85090
  }
85054
85091
  return {
85055
- installationId: approvedInstallation,
85056
- clientId,
85057
- tokenEndpoint: device.resource,
85092
+ installationId: opts.installationId,
85093
+ clientId: opts.clientId,
85094
+ tokenEndpoint: opts.resourceUrl,
85058
85095
  tokens: {
85059
85096
  accessToken: resource["access_token"],
85060
- refreshToken: deviceSession,
85097
+ refreshToken: opts.deviceSession,
85061
85098
  ...typeof resource["expires_in"] === "number" ? { expiresInSec: resource["expires_in"] } : {}
85062
85099
  }
85063
85100
  };
@@ -85271,7 +85308,10 @@ async function linkThroughConsole(opts) {
85271
85308
  return {
85272
85309
  status: "approved",
85273
85310
  decision,
85274
- installationId: typeof payload?.["installation_id"] === "string" ? payload["installation_id"] : null
85311
+ installationId: typeof payload?.["installation_id"] === "string" ? payload["installation_id"] : null,
85312
+ deviceSession: typeof payload?.["access_token"] === "string" ? payload["access_token"] : null,
85313
+ resourceUrl: device.resource,
85314
+ clientId
85275
85315
  };
85276
85316
  }
85277
85317
  return { status: "expired" };
@@ -86462,6 +86502,8 @@ async function startMultiDaemon(opts = {}) {
86462
86502
  );
86463
86503
  } else if (out2.reason === "error") {
86464
86504
  console.warn(`[errata] project link failed for ${r.engine.profile.name}: ${out2.detail}`);
86505
+ } else if (out2.reason === "held") {
86506
+ noteProjectHold(r.id, r.engine.profile.name, out2.detail);
86465
86507
  }
86466
86508
  let primeSalt = null;
86467
86509
  if (r.engine.profile.projectId && !r.engine.profile.projectHydratedAt) {
@@ -86919,7 +86961,16 @@ async function startMultiDaemon(opts = {}) {
86919
86961
  const client = poolFor(recordIdentity);
86920
86962
  const store = r.engine.store;
86921
86963
  const projectName = r.engine.profile.name ?? r.engine.profile.id;
86922
- const onDiskProjectId = loadProfile(r.root)?.projectId;
86964
+ const onDisk = loadProfile(r.root);
86965
+ const onDiskProjectId = onDisk?.projectId;
86966
+ if (!onDiskProjectId && onDisk?.projectLinkHold) {
86967
+ noteProjectHold(
86968
+ r.id,
86969
+ r.entry.name,
86970
+ "this repo is already on errata in another workspace \u2014 run `errata link` to join it or create your own project"
86971
+ );
86972
+ continue;
86973
+ }
86923
86974
  if (onDiskProjectId !== r.engine.profile.projectId) {
86924
86975
  if (onDiskProjectId) r.engine.profile.projectId = onDiskProjectId;
86925
86976
  else delete r.engine.profile.projectId;
@@ -88679,6 +88730,11 @@ async function cmdStatus() {
88679
88730
  console.log(
88680
88731
  ` repo: ${profile.repoLocator ? `${profile.repoLocator}${link}` : "(no git remote \u2014 unlinked)"}`
88681
88732
  );
88733
+ if (!profile.projectId && profile.projectLinkHold) {
88734
+ console.log(
88735
+ ` HELD: this repo is already on errata in another workspace. Nothing syncs until you decide: run \`errata link\` to join it or create your own project.`
88736
+ );
88737
+ }
88682
88738
  for (const h of await daemonHolds()) {
88683
88739
  if (h.workspace !== profile.name && h.workspace !== profile.id) continue;
88684
88740
  console.log(
@@ -89261,6 +89317,50 @@ async function linkRegisteredWorkspaces(cfg) {
89261
89317
  }
89262
89318
  }
89263
89319
  }
89320
+ async function bindWorkspaceToLinkInstallation(cfg, out2, orgId) {
89321
+ const installationId = out2.installationId;
89322
+ if (!out2.deviceSession) {
89323
+ console.warn(
89324
+ ` note: the project is in another org; run \`errata login --device --installation ${installationId} --profile <name>\` then \`errata workspace bind . --profile <name>\``
89325
+ );
89326
+ return null;
89327
+ }
89328
+ try {
89329
+ const minted = await exchangeDeviceSession({
89330
+ resourceUrl: out2.resourceUrl,
89331
+ deviceSession: out2.deviceSession,
89332
+ installationId,
89333
+ clientId: out2.clientId
89334
+ });
89335
+ const existing = Object.entries(cfg.installationProfiles).find(([, p]) => p.installationId === installationId)?.[0];
89336
+ let profileName = existing ?? `link-${orgId.slice(0, 8)}`;
89337
+ for (let i2 = 2; !existing && cfg.installationProfiles[profileName]; i2++) profileName = `link-${orgId.slice(0, 8)}-${i2}`;
89338
+ const next = {
89339
+ ...cfg,
89340
+ installationProfiles: {
89341
+ ...cfg.installationProfiles,
89342
+ [profileName]: {
89343
+ installationId,
89344
+ cloudUrl: cfg.cloudUrl,
89345
+ apiKey: null,
89346
+ accessToken: minted.tokens.accessToken,
89347
+ refreshToken: minted.tokens.refreshToken ?? null,
89348
+ tokenEndpoint: minted.tokenEndpoint,
89349
+ oauthClientId: minted.clientId,
89350
+ activeAgent: null
89351
+ }
89352
+ }
89353
+ };
89354
+ saveConfig(next);
89355
+ setWorkspaceBinding(workspaceEntryForPath(ROOT)?.id ?? ROOT, profileName);
89356
+ return { config: next, profileName };
89357
+ } catch (err2) {
89358
+ console.warn(
89359
+ ` note: the project is linked, but this workspace could not take the other org's credential (${err2 instanceof Error ? err2.message : String(err2)}); run \`errata login --device --installation ${installationId} --profile <name>\` then \`errata workspace bind . --profile <name>\``
89360
+ );
89361
+ return null;
89362
+ }
89363
+ }
89264
89364
  async function cmdLink(args2) {
89265
89365
  const flag = (name2) => {
89266
89366
  const i2 = args2.indexOf(name2);
@@ -89280,6 +89380,7 @@ async function cmdLink(args2) {
89280
89380
  return;
89281
89381
  }
89282
89382
  delete profile.projectLinkDisabled;
89383
+ delete profile.projectLinkHold;
89283
89384
  const remote = flag("--remote");
89284
89385
  if (remote) {
89285
89386
  const loc = detectRepoLocator(ROOT, remote);
@@ -89340,12 +89441,22 @@ async function cmdLink(args2) {
89340
89441
  );
89341
89442
  await refreshLinkedCodexHooks(args2);
89342
89443
  if (applied.wroteIgnore) console.log("wrote .errataignore (starter) \u2014 edit it; it is your hard control");
89444
+ let reportClient = authedCloudClient(applied.config);
89445
+ if (out3.installationId && out3.installationId !== activeProfile?.installationId) {
89446
+ const bound = await bindWorkspaceToLinkInstallation(applied.config, out3, d.orgId);
89447
+ if (bound) {
89448
+ reportClient = authedCloudClientForProfile(bound.config, bound.profileName);
89449
+ console.log(
89450
+ `this workspace now uses installation ${out3.installationId.slice(0, 8)} (profile ${bound.profileName}) in org ${d.orgId.slice(0, 8)}; your machine default is unchanged`
89451
+ );
89452
+ }
89453
+ }
89343
89454
  if (!hasCloudCredential(cfg)) {
89344
89455
  console.log(" note: run `errata login` so this daemon can sync under the link and report the history stage.");
89345
89456
  return;
89346
89457
  }
89347
89458
  const consents = historyConsents(applied.profile);
89348
- const reported = await reportHistoryStage(authedCloudClient(applied.config), applied.profile, (line) => console.warn(line));
89459
+ const reported = await reportHistoryStage(reportClient, applied.profile, (line) => console.warn(line));
89349
89460
  if (reported?.consents) {
89350
89461
  const synced = applyConsents(applied.profile, applied.config, reported.consents, (/* @__PURE__ */ new Date()).toISOString());
89351
89462
  if (synced.changed) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.7-dev.2797",
3
+ "version": "2.0.7-dev.2822",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {