@agentxm/extension-sources 0.28.4 → 0.28.5

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.
@@ -82,7 +82,7 @@ export declare class SourceNetworkFailure extends SourceNetworkFailure_base<{
82
82
  }> {
83
83
  }
84
84
  /** The git subprocess operations this package performs. */
85
- export type GitOperation = "clone" | "get-commit-sha" | "get-tree-sha";
85
+ export type GitOperation = "clone" | "get-commit-sha" | "get-tree-sha" | "compare-directory-to-head";
86
86
  declare const GitOperationFailed_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
87
87
  readonly _tag: "GitOperationFailed";
88
88
  } & Readonly<A>;
@@ -0,0 +1,19 @@
1
+ import * as ServiceMap from "effect/Context";
2
+ import type * as Effect from "effect/Effect";
3
+ import * as Option from "effect/Option";
4
+ import type { GitOperationFailed } from "../errors.js";
5
+ import type { GitDirectoryComparisonResult } from "./operations.js";
6
+ /** Input material for comparing one directory with its enclosing Git HEAD. */
7
+ export interface GitDirectoryComparisonInput {
8
+ readonly directory: string;
9
+ readonly currentPaths: ReadonlyArray<string>;
10
+ }
11
+ /** Git worktree comparison capability used by publication preflight. */
12
+ export interface GitDirectoryComparisonService {
13
+ readonly compare: (input: GitDirectoryComparisonInput) => Effect.Effect<Option.Option<GitDirectoryComparisonResult>, GitOperationFailed>;
14
+ }
15
+ declare const GitDirectoryComparison_base: ServiceMap.ServiceClass<GitDirectoryComparison, "@agentxm/extension-sources/git/GitDirectoryComparison", GitDirectoryComparisonService>;
16
+ export declare class GitDirectoryComparison extends GitDirectoryComparison_base {
17
+ }
18
+ export {};
19
+ //# sourceMappingURL=directory-comparison.d.ts.map
@@ -0,0 +1,5 @@
1
+ import * as ServiceMap from "effect/Context";
2
+ import * as Option from "effect/Option";
3
+ export class GitDirectoryComparison extends ServiceMap.Service()("@agentxm/extension-sources/git/GitDirectoryComparison") {
4
+ }
5
+ //# sourceMappingURL=directory-comparison.js.map
@@ -7,6 +7,20 @@
7
7
  import * as Effect from "effect/Effect";
8
8
  import * as Path from "effect/Path";
9
9
  import { GitOperationFailed } from "../errors.js";
10
+ /** One raw regular-file difference between a Git HEAD subtree and the working tree. */
11
+ export interface GitDirectoryDifference {
12
+ readonly path: string;
13
+ readonly change: "added" | "modified" | "deleted";
14
+ readonly headObject?: string;
15
+ readonly workingObject?: string;
16
+ }
17
+ /** Git evidence for one directory, before feature-specific archive filtering. */
18
+ export interface GitDirectoryComparisonResult {
19
+ readonly repositoryRoot: string;
20
+ readonly repositoryDirectory: string;
21
+ readonly headRevision?: string;
22
+ readonly differences: ReadonlyArray<GitDirectoryDifference>;
23
+ }
10
24
  /**
11
25
  * Shallow clone a git repository (depth 1, single branch).
12
26
  * Significantly faster than a full clone for read-only use cases like skill discovery.
@@ -34,4 +48,24 @@ export declare const getCommitSha: (repoPath: string) => Effect.Effect<string, G
34
48
  * @experimental This API is unstable and may change without notice.
35
49
  */
36
50
  export declare const getTreeSha: (repoPath: string, subPath?: string) => Effect.Effect<string, GitOperationFailed, never>;
51
+ /**
52
+ * Compare an exact set of current regular files with the corresponding Git
53
+ * HEAD subtree. The caller owns which current paths belong to its material
54
+ * boundary; deleted HEAD paths remain present in the returned difference set
55
+ * so that boundary can classify them too.
56
+ */
57
+ export declare const compareDirectoryToHead: (repositoryRoot: string, directory: string, currentPaths: ReadonlyArray<string>) => Effect.Effect<{
58
+ repositoryRoot: string;
59
+ repositoryDirectory: string;
60
+ differences: {
61
+ path: string;
62
+ change: "added";
63
+ }[];
64
+ headRevision?: never;
65
+ } | {
66
+ repositoryRoot: string;
67
+ repositoryDirectory: string;
68
+ headRevision: string;
69
+ differences: GitDirectoryDifference[];
70
+ }, GitOperationFailed, Path.Path>;
37
71
  //# sourceMappingURL=operations.d.ts.map
@@ -33,6 +33,75 @@ const mapGitError = (operation, context) => (error) => {
33
33
  cause: error,
34
34
  });
35
35
  };
36
+ const toPosixPath = (value, separator) => separator === "/" ? value : value.split(separator).join("/");
37
+ const relativeTreePath = (repositoryDirectory, repositoryPath) => {
38
+ if (repositoryDirectory === ".")
39
+ return repositoryPath;
40
+ const prefix = `${repositoryDirectory}/`;
41
+ return repositoryPath.startsWith(prefix) ? repositoryPath.slice(prefix.length) : undefined;
42
+ };
43
+ const parseHeadBlobs = (output, repositoryDirectory) => {
44
+ const blobs = new Map();
45
+ for (const record of output.split("\0")) {
46
+ if (record.length === 0)
47
+ continue;
48
+ const separator = record.indexOf("\t");
49
+ if (separator < 0)
50
+ throw new Error(`Unexpected ls-tree output: ${record}`);
51
+ const [mode, objectType, objectId] = record.slice(0, separator).split(" ");
52
+ if (mode === undefined || objectType === undefined || objectId === undefined) {
53
+ throw new Error(`Unexpected ls-tree output: ${record}`);
54
+ }
55
+ if (objectType !== "blob" || !mode.startsWith("100"))
56
+ continue;
57
+ const path = relativeTreePath(repositoryDirectory, record.slice(separator + 1));
58
+ if (path !== undefined)
59
+ blobs.set(path, objectId);
60
+ }
61
+ return blobs;
62
+ };
63
+ const hashWorkingFiles = async (git, directory, paths) => {
64
+ if (paths.length === 0)
65
+ return new Map();
66
+ const workingBlobs = new Map();
67
+ for (let offset = 0; offset < paths.length; offset += 128) {
68
+ const batch = paths.slice(offset, offset + 128);
69
+ const output = await git.raw([
70
+ "hash-object",
71
+ "--no-filters",
72
+ "--",
73
+ ...batch.map((path) => `${directory}/${path}`),
74
+ ]);
75
+ const hashes = output.trimEnd().split("\n");
76
+ if (hashes.length !== batch.length) {
77
+ throw new Error(`Expected ${batch.length} working-tree hashes, received ${hashes.length}`);
78
+ }
79
+ for (const [index, path] of batch.entries()) {
80
+ const hash = hashes[index];
81
+ if (hash === undefined)
82
+ throw new Error(`Missing working-tree hash for '${path}'`);
83
+ workingBlobs.set(path, hash);
84
+ }
85
+ }
86
+ return workingBlobs;
87
+ };
88
+ const readHeadRevision = async (git) => {
89
+ try {
90
+ return (await git.revparse(["--verify", "HEAD"])).trim();
91
+ }
92
+ catch (cause) {
93
+ try {
94
+ const symbolicRef = (await git.raw(["symbolic-ref", "--quiet", "HEAD"])).trim();
95
+ const refObject = (await git.raw(["for-each-ref", "--format=%(objectname)", symbolicRef])).trim();
96
+ if (refObject.length === 0)
97
+ return undefined;
98
+ throw cause;
99
+ }
100
+ catch {
101
+ throw cause;
102
+ }
103
+ }
104
+ };
36
105
  // -----------------------------------------------------------------------------
37
106
  // Git Operations
38
107
  // -----------------------------------------------------------------------------
@@ -98,4 +167,64 @@ export const getTreeSha = (repoPath, subPath = ".") => Effect.tryPromise({
98
167
  },
99
168
  catch: mapGitError("get-tree-sha", `Failed to get tree SHA for '${subPath}'`),
100
169
  }).pipe(Effect.withSpan("Git.getTreeSha"));
170
+ /**
171
+ * Compare an exact set of current regular files with the corresponding Git
172
+ * HEAD subtree. The caller owns which current paths belong to its material
173
+ * boundary; deleted HEAD paths remain present in the returned difference set
174
+ * so that boundary can classify them too.
175
+ */
176
+ export const compareDirectoryToHead = (repositoryRoot, directory, currentPaths) => Effect.gen(function* () {
177
+ const path = yield* Path.Path;
178
+ return yield* Effect.tryPromise({
179
+ try: async (signal) => {
180
+ const git = createGit(repositoryRoot, signal);
181
+ const repositoryDirectory = toPosixPath(path.relative(repositoryRoot, directory), path.sep);
182
+ const normalizedDirectory = repositoryDirectory.length === 0 ? "." : repositoryDirectory;
183
+ const headRevision = await readHeadRevision(git);
184
+ if (headRevision === undefined) {
185
+ return {
186
+ repositoryRoot,
187
+ repositoryDirectory: normalizedDirectory,
188
+ differences: [...currentPaths]
189
+ .sort((left, right) => left.localeCompare(right))
190
+ .map((currentPath) => ({ path: currentPath, change: "added" })),
191
+ };
192
+ }
193
+ const treeOutput = await git.raw([
194
+ "ls-tree",
195
+ "-r",
196
+ "-z",
197
+ "--full-tree",
198
+ "HEAD",
199
+ "--",
200
+ normalizedDirectory,
201
+ ]);
202
+ const headBlobs = parseHeadBlobs(treeOutput, normalizedDirectory);
203
+ const workingBlobs = await hashWorkingFiles(git, directory, currentPaths);
204
+ const allPaths = [...new Set([...headBlobs.keys(), ...workingBlobs.keys()])].sort((left, right) => left.localeCompare(right));
205
+ const differences = allPaths.flatMap((currentPath) => {
206
+ const headObject = headBlobs.get(currentPath);
207
+ const workingObject = workingBlobs.get(currentPath);
208
+ if (headObject === workingObject)
209
+ return [];
210
+ if (headObject === undefined) {
211
+ return workingObject === undefined
212
+ ? []
213
+ : [{ path: currentPath, change: "added", workingObject }];
214
+ }
215
+ if (workingObject === undefined) {
216
+ return [{ path: currentPath, change: "deleted", headObject }];
217
+ }
218
+ return [{ path: currentPath, change: "modified", headObject, workingObject }];
219
+ });
220
+ return {
221
+ repositoryRoot,
222
+ repositoryDirectory: normalizedDirectory,
223
+ headRevision,
224
+ differences,
225
+ };
226
+ },
227
+ catch: mapGitError("compare-directory-to-head", `Failed to compare '${directory}' with Git HEAD`),
228
+ });
229
+ }).pipe(Effect.withSpan("Git.compareDirectoryToHead"));
101
230
  //# sourceMappingURL=operations.js.map
@@ -26,5 +26,6 @@ export { discoverExtensionPackages, inspectExtensionPackage, type DiscoveredExte
26
26
  export { acquireExternalSource, findExtensionPackagesFromSource, type AcquiredExternalSource, type ResolvedExtensionPackage, } from "./package-sources.js";
27
27
  export { fileUrlToPath } from "./file-url.js";
28
28
  export { findGitRoot, isGitManaged } from "./git/detect.js";
29
- export { getCommitSha, getTreeSha, shallowClone } from "./git/operations.js";
29
+ export { compareDirectoryToHead, getCommitSha, getTreeSha, shallowClone, type GitDirectoryComparisonResult, type GitDirectoryDifference, } from "./git/operations.js";
30
+ export { GitDirectoryComparison, type GitDirectoryComparisonInput, type GitDirectoryComparisonService, } from "./git/directory-comparison.js";
30
31
  //# sourceMappingURL=index.d.ts.map
package/dist/src/index.js CHANGED
@@ -31,5 +31,6 @@ export { acquireExternalSource, findExtensionPackagesFromSource, } from "./packa
31
31
  export { fileUrlToPath } from "./file-url.js";
32
32
  // Git acquisition
33
33
  export { findGitRoot, isGitManaged } from "./git/detect.js";
34
- export { getCommitSha, getTreeSha, shallowClone } from "./git/operations.js";
34
+ export { compareDirectoryToHead, getCommitSha, getTreeSha, shallowClone, } from "./git/operations.js";
35
+ export { GitDirectoryComparison, } from "./git/directory-comparison.js";
35
36
  //# sourceMappingURL=index.js.map
@@ -15,6 +15,7 @@ import * as Layer from "effect/Layer";
15
15
  import { AxmSkillCandidateGate } from "./axm-skill-gate.js";
16
16
  import { SourceHostProviders } from "./service.js";
17
17
  import { WorkspaceCatalog } from "./workspace-catalog.js";
18
+ import { GitDirectoryComparison } from "./git/directory-comparison.js";
18
19
  /**
19
20
  * Live layer for SourceHostProviders.
20
21
  *
@@ -26,4 +27,6 @@ import { WorkspaceCatalog } from "./workspace-catalog.js";
26
27
  * @experimental This API is unstable and may change without notice.
27
28
  */
28
29
  export declare const SourceHostProvidersLive: Layer.Layer<SourceHostProviders, never, FileSystem.FileSystem | HttpClient.HttpClient | Path.Path | WorkspaceCatalog | AxmSkillCandidateGate>;
30
+ /** Live local-Git comparison service. */
31
+ export declare const GitDirectoryComparisonLive: Layer.Layer<GitDirectoryComparison, never, FileSystem.FileSystem | Path.Path>;
29
32
  //# sourceMappingURL=live.d.ts.map
package/dist/src/live.js CHANGED
@@ -23,6 +23,9 @@ import { createGitHostingSourceHostProvider } from "./providers/git-hosting.js";
23
23
  import { createLocalSourceHostProvider } from "./providers/local.js";
24
24
  import { buildCloneUrlFromSource, createRegistryMetaProvider, getOriginFromSource, SourceHostProviders, } from "./service.js";
25
25
  import { WorkspaceCatalog } from "./workspace-catalog.js";
26
+ import { GitDirectoryComparison } from "./git/directory-comparison.js";
27
+ import { compareDirectoryToHead } from "./git/operations.js";
28
+ import { findGitRoot } from "./git/detect.js";
26
29
  // -----------------------------------------------------------------------------
27
30
  // Layer
28
31
  // -----------------------------------------------------------------------------
@@ -128,4 +131,16 @@ export const SourceHostProvidersLive = Layer.effect(SourceHostProviders, Effect.
128
131
  };
129
132
  return service;
130
133
  }));
134
+ /** Live local-Git comparison service. */
135
+ export const GitDirectoryComparisonLive = Layer.effect(GitDirectoryComparison, Effect.gen(function* () {
136
+ const fs = yield* FileSystem.FileSystem;
137
+ const path = yield* Path.Path;
138
+ const platform = Layer.mergeAll(Layer.succeed(FileSystem.FileSystem, fs), Layer.succeed(Path.Path, path));
139
+ return {
140
+ compare: ({ directory, currentPaths }) => findGitRoot(directory).pipe(Effect.flatMap(Option.match({
141
+ onNone: () => Effect.succeed(Option.none()),
142
+ onSome: (repositoryRoot) => compareDirectoryToHead(repositoryRoot, directory, currentPaths).pipe(Effect.map(Option.some)),
143
+ })), Effect.provide(platform)),
144
+ };
145
+ }));
131
146
  //# sourceMappingURL=live.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentxm/extension-sources",
3
- "version": "0.28.4",
3
+ "version": "0.28.5",
4
4
  "description": "AXM extension-source integration: source locator routing over the contract grammar, host providers for Registry, GitHub, GitLab, Bitbucket, Azure Repos, generic Git, and local paths, convention and manifest package discovery, identifier resolution, and shallow git acquisition for the axm CLI. Unstable and unsupported — use the axm.sh CLI.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-MIT",
@@ -18,10 +18,12 @@
18
18
  "exports": {
19
19
  ".": {
20
20
  "types": "./dist/src/index.d.ts",
21
+ "axm-source": "./src/index.ts",
21
22
  "default": "./dist/src/index.js"
22
23
  },
23
24
  "./live": {
24
25
  "types": "./dist/src/live.d.ts",
26
+ "axm-source": "./src/live.ts",
25
27
  "default": "./dist/src/live.js"
26
28
  }
27
29
  },
@@ -42,9 +44,9 @@
42
44
  "effect": "4.0.0-rc.112",
43
45
  "semver": "^7.8.5",
44
46
  "simple-git": "^3.36.0",
45
- "@agentxm/extension-model": "^0.28.4",
46
- "@agentxm/registry-client": "^0.28.4",
47
- "@agentxm/registry-protocol": "^0.28.4"
47
+ "@agentxm/registry-client": "^0.28.5",
48
+ "@agentxm/extension-model": "^0.28.5",
49
+ "@agentxm/registry-protocol": "^0.28.5"
48
50
  },
49
51
  "devDependencies": {
50
52
  "@effect/platform-node": "4.0.0-rc.112",