@remogram/core 0.1.0-beta.1 → 0.1.0-beta.2

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.
@@ -0,0 +1,44 @@
1
+ /** Per-command auth requirements for structured provider capabilities. */
2
+ export const AUTH_CLASS = {
3
+ NONE: 'none',
4
+ GIT_ONLY: 'git_only',
5
+ TOKEN_REQUIRED: 'token_required',
6
+ };
7
+
8
+ const AUTH_CLASS_VALUES = new Set(Object.values(AUTH_CLASS));
9
+
10
+ /** Runtime auth requirements for fully implemented API providers. */
11
+ export const API_PROVIDER_COMMAND_AUTH = {
12
+ repo_status: AUTH_CLASS.NONE,
13
+ ref_compare: AUTH_CLASS.GIT_ONLY,
14
+ pr_status: AUTH_CLASS.TOKEN_REQUIRED,
15
+ pr_checks: AUTH_CLASS.TOKEN_REQUIRED,
16
+ merge_plan: AUTH_CLASS.TOKEN_REQUIRED,
17
+ sync_plan: AUTH_CLASS.GIT_ONLY,
18
+ };
19
+
20
+ export function commandCapability(name, { implemented = true } = {}) {
21
+ const auth_class = API_PROVIDER_COMMAND_AUTH[name];
22
+ if (!auth_class) {
23
+ throw new Error(`Unknown command: ${name}`);
24
+ }
25
+ return { name, implemented, auth_class };
26
+ }
27
+
28
+ export function apiProviderCommands() {
29
+ return Object.keys(API_PROVIDER_COMMAND_AUTH).map((name) =>
30
+ commandCapability(name, { implemented: true }),
31
+ );
32
+ }
33
+
34
+ export function stubProviderCommands() {
35
+ return Object.keys(API_PROVIDER_COMMAND_AUTH).map((name) =>
36
+ commandCapability(name, { implemented: false }),
37
+ );
38
+ }
39
+
40
+ export function assertAuthClass(value) {
41
+ if (!AUTH_CLASS_VALUES.has(value)) {
42
+ throw new Error(`Invalid auth_class: ${value}`);
43
+ }
44
+ }
package/git-local.js CHANGED
@@ -24,6 +24,14 @@ export function gitCurrentBranch(cwd) {
24
24
  }
25
25
  }
26
26
 
27
+ export function gitRepoRoot(cwd) {
28
+ try {
29
+ return gitExec(cwd, ['rev-parse', '--show-toplevel']);
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
27
35
  export function gitAheadBehind(cwd, base, head) {
28
36
  assertGitRef(base, 'base');
29
37
  assertGitRef(head, 'head');
package/index.js CHANGED
@@ -12,7 +12,15 @@ export {
12
12
  forgeIngestCapabilityFacts,
13
13
  } from './caps.js';
14
14
  export { assertGitRef, assertGitRemote } from './git-args.js';
15
- export { gitRevParse, gitCurrentBranch, gitAheadBehind } from './git-local.js';
15
+ export { gitRevParse, gitCurrentBranch, gitAheadBehind, gitRepoRoot } from './git-local.js';
16
+ export {
17
+ localHeadShaForPr,
18
+ staleHeadDetails,
19
+ staleHeadForgeError,
20
+ staleHeadForgeError as staleHeadError,
21
+ STALE_HEAD_MESSAGE,
22
+ throwIfStaleHeadByNumber,
23
+ } from './pr-head-reconcile.js';
16
24
  export { parseConfigFile, configSchema } from './config-schema.js';
17
25
  export {
18
26
  findConfigPath,
@@ -25,3 +33,11 @@ export {
25
33
  forgeContext,
26
34
  } from './resolve.js';
27
35
  export { fetchWithTimeout, fetchJson, fetchJsonWithMeta, parseLinkHeader, fetchTextCapped } from './http.js';
36
+ export {
37
+ AUTH_CLASS,
38
+ API_PROVIDER_COMMAND_AUTH,
39
+ commandCapability,
40
+ apiProviderCommands,
41
+ stubProviderCommands,
42
+ assertAuthClass,
43
+ } from './auth-classes.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remogram/core",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.2",
4
4
  "description": "Remogram forge envelope, config, caps, and HTTP utilities",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,38 @@
1
+ import { assertGitRemote } from './git-args.js';
2
+ import { ERROR_CODES, forgeError } from './contracts/errors.js';
3
+ import { gitRevParse } from './git-local.js';
4
+
5
+ export const STALE_HEAD_MESSAGE =
6
+ 'Forge PR head SHA diverges from locally resolved git; fetch or refresh before trusting head_sha';
7
+
8
+ export function localHeadShaForPr(cwd, remoteName, headRef) {
9
+ if (!headRef) return null;
10
+ assertGitRemote(remoteName, 'remote');
11
+ const trackingRef = `${remoteName}/${headRef}`;
12
+ return gitRevParse(cwd, trackingRef) ?? gitRevParse(cwd, headRef);
13
+ }
14
+
15
+ export function staleHeadDetails(cwd, remoteName, headRef, forgeHeadSha) {
16
+ if (!headRef || !forgeHeadSha) return null;
17
+ const localHeadSha = localHeadShaForPr(cwd, remoteName, headRef);
18
+ if (!localHeadSha) return null;
19
+ if (localHeadSha.toLowerCase() === String(forgeHeadSha).toLowerCase()) return null;
20
+ return {
21
+ head_ref: headRef,
22
+ head_sha: forgeHeadSha,
23
+ local_head_sha: localHeadSha,
24
+ };
25
+ }
26
+
27
+ export function staleHeadForgeError() {
28
+ return forgeError(ERROR_CODES.STALE_HEAD, STALE_HEAD_MESSAGE);
29
+ }
30
+
31
+ export function throwIfStaleHeadByNumber(ctx, packetType, body, headRef, forgeHeadSha) {
32
+ const details = staleHeadDetails(ctx.cwd, ctx.config?.remote ?? ctx.remoteName, headRef, forgeHeadSha);
33
+ if (!details) return;
34
+ const err = new Error(STALE_HEAD_MESSAGE);
35
+ err.forgeError = staleHeadForgeError();
36
+ err.staleHeadPacket = { type: packetType, body: { ...body, ...details } };
37
+ throw err;
38
+ }
package/resolve.js CHANGED
@@ -1,20 +1,35 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { readFileSync, existsSync } from 'node:fs';
3
- import { dirname, join } from 'node:path';
2
+ import { readFileSync, existsSync, realpathSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
4
  import { parseConfigFile } from './config-schema.js';
5
5
  import { ERROR_CODES, forgeError } from './contracts/errors.js';
6
6
  import { assertGitRemote } from './git-args.js';
7
+ import { gitRepoRoot } from './git-local.js';
7
8
 
8
9
  const HOST_ALIASES = new Map([
9
10
  ['localhost:3000', '127.0.0.1:3000'],
10
11
  ['127.0.0.1:3000', 'localhost:3000'],
11
12
  ]);
12
13
 
14
+ function samePath(a, b) {
15
+ try {
16
+ return realpathSync(a) === realpathSync(b);
17
+ } catch {
18
+ return resolve(a) === resolve(b);
19
+ }
20
+ }
21
+
13
22
  export function findConfigPath(startDir = process.cwd()) {
23
+ const repoRoot = gitRepoRoot(startDir);
14
24
  let dir = startDir;
15
25
  while (true) {
16
26
  const candidate = join(dir, '.remogram.json');
17
27
  if (existsSync(candidate)) return candidate;
28
+ if (repoRoot) {
29
+ if (samePath(dir, repoRoot)) break;
30
+ } else {
31
+ break;
32
+ }
18
33
  const parent = dirname(dir);
19
34
  if (parent === dir) break;
20
35
  dir = parent;
package/stub-provider.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ERROR_CODES, forgeError } from './contracts/errors.js';
2
+ import { stubProviderCommands } from './auth-classes.js';
2
3
 
3
4
  export function createStubProvider(id) {
4
5
  function unsupported() {
@@ -8,14 +9,7 @@ export function createStubProvider(id) {
8
9
  }
9
10
  function providerCapabilities() {
10
11
  return {
11
- commands: [
12
- { name: 'repo_status', implemented: false },
13
- { name: 'ref_compare', implemented: false },
14
- { name: 'pr_status', implemented: false },
15
- { name: 'pr_checks', implemented: false },
16
- { name: 'merge_plan', implemented: false },
17
- { name: 'sync_plan', implemented: false },
18
- ],
12
+ commands: stubProviderCommands(),
19
13
  auth_envs: [],
20
14
  check_sources: [],
21
15
  mergeability_confidence: 'unknown',