@aigne/afs-git 1.11.0-beta.6 → 1.11.0-beta.7

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/index.mjs CHANGED
@@ -6,7 +6,7 @@ import { tmpdir } from "node:os";
6
6
  import { basename, dirname, isAbsolute, join } from "node:path";
7
7
  import { promisify } from "node:util";
8
8
  import { AFSNotFoundError } from "@aigne/afs";
9
- import { AFSBaseProvider, Delete, List, Meta, Read, Rename, Search, Stat, Write } from "@aigne/afs/provider";
9
+ import { AFSBaseProvider, Actions, Delete, Explain, List, Meta, Read, Rename, Search, Stat, Write } from "@aigne/afs/provider";
10
10
  import { camelize, optionalize, zodParse } from "@aigne/afs/utils/zod";
11
11
  import { simpleGit } from "simple-git";
12
12
  import { z } from "zod";
@@ -37,10 +37,10 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
37
37
  static schema() {
38
38
  return afsGitOptionsSchema;
39
39
  }
40
- static async load({ filepath, parsed }) {
40
+ static async load({ basePath, config } = {}) {
41
41
  const instance = new AFSGit({
42
- ...await AFSGit.schema().parseAsync(parsed),
43
- cwd: dirname(filepath)
42
+ ...await AFSGit.schema().parseAsync(config),
43
+ cwd: basePath
44
44
  });
45
45
  await instance.ready();
46
46
  return instance;
@@ -145,21 +145,24 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
145
145
  }
146
146
  /**
147
147
  * List root (branches)
148
+ * Note: list() returns only children (branches), never the path itself (per new semantics)
148
149
  */
149
150
  async listRootHandler(ctx) {
150
151
  await this.ready();
151
152
  const options = ctx.options;
152
153
  const maxDepth = options?.maxDepth ?? 1;
153
154
  const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
155
+ if (maxDepth === 0) return { data: [] };
154
156
  const branches = await this.getBranches();
155
- if (maxDepth === 0) return { data: [this.buildEntry("/", { metadata: { childrenCount: branches.length } })] };
156
157
  const entries = [];
157
- entries.push(this.buildEntry("/", { metadata: { childrenCount: branches.length } }));
158
158
  for (const name of branches) {
159
159
  if (entries.length >= limit) break;
160
160
  const encodedPath = this.buildBranchPath(name);
161
161
  const branchChildrenCount = await this.getChildrenCount(name, "");
162
- entries.push(this.buildEntry(encodedPath, { metadata: { childrenCount: branchChildrenCount } }));
162
+ entries.push(this.buildEntry(encodedPath, { meta: {
163
+ kind: "git:branch",
164
+ childrenCount: branchChildrenCount
165
+ } }));
163
166
  if (maxDepth > 1) {
164
167
  const branchResult = await this.listWithGitLsTree(name, "", {
165
168
  maxDepth: maxDepth - 1,
@@ -195,7 +198,7 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
195
198
  async readRootMetaHandler(_ctx) {
196
199
  await this.ready();
197
200
  const branches = await this.getBranches();
198
- return this.buildEntry("/.meta", { metadata: {
201
+ return this.buildEntry("/.meta", { meta: {
199
202
  childrenCount: branches.length,
200
203
  type: "root"
201
204
  } });
@@ -209,9 +212,11 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
209
212
  await this.ensureBranchExists(branch);
210
213
  const childrenCount = await this.getChildrenCount(branch, "");
211
214
  const metaPath = `${`/${this.encodeBranchName(branch)}`}/.meta`;
212
- return this.buildEntry(metaPath, { metadata: {
215
+ const lastCommit = await this.getLastCommit(branch);
216
+ return this.buildEntry(metaPath, { meta: {
213
217
  childrenCount,
214
- type: "branch"
218
+ type: "branch",
219
+ lastCommit
215
220
  } });
216
221
  }
217
222
  /**
@@ -232,7 +237,7 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
232
237
  const metaPath = `/${this.encodeBranchName(branch)}/${filePath}/.meta`;
233
238
  let childrenCount;
234
239
  if (isDir) childrenCount = await this.getChildrenCount(branch, filePath);
235
- return this.buildEntry(metaPath, { metadata: {
240
+ return this.buildEntry(metaPath, { meta: {
236
241
  childrenCount,
237
242
  type: isDir ? "directory" : "file",
238
243
  gitObjectType: objectType
@@ -244,7 +249,7 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
244
249
  async readRootHandler(_ctx) {
245
250
  await this.ready();
246
251
  const branches = await this.getBranches();
247
- return this.buildEntry("/", { metadata: { childrenCount: branches.length } });
252
+ return this.buildEntry("/", { meta: { childrenCount: branches.length } });
248
253
  }
249
254
  /**
250
255
  * Read branch root
@@ -255,7 +260,11 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
255
260
  await this.ensureBranchExists(branch);
256
261
  const branchPath = this.buildBranchPath(branch);
257
262
  const childrenCount = await this.getChildrenCount(branch, "");
258
- return this.buildEntry(branchPath, { metadata: { childrenCount } });
263
+ const lastCommit = await this.getLastCommit(branch);
264
+ return this.buildEntry(branchPath, { meta: {
265
+ childrenCount,
266
+ lastCommit
267
+ } });
259
268
  }
260
269
  /**
261
270
  * Read file or directory in branch
@@ -272,23 +281,23 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
272
281
  if (stats.isDirectory()) {
273
282
  const files = await readdir(fullPath);
274
283
  const afsPath$2 = this.buildBranchPath(branch, filePath);
275
- return this.buildEntry(afsPath$2, { metadata: { childrenCount: files.length } });
284
+ return this.buildEntry(afsPath$2, { meta: { childrenCount: files.length } });
276
285
  }
277
286
  const mimeType$1 = this.getMimeType(filePath);
278
287
  const isBinary$1 = this.isBinaryFile(filePath);
279
288
  let content$1;
280
- const metadata$1 = {
289
+ const meta$1 = {
281
290
  size: stats.size,
282
291
  mimeType: mimeType$1
283
292
  };
284
293
  if (isBinary$1) {
285
294
  content$1 = (await readFile(fullPath)).toString("base64");
286
- metadata$1.contentType = "base64";
295
+ meta$1.contentType = "base64";
287
296
  } else content$1 = await readFile(fullPath, "utf8");
288
297
  const afsPath$1 = this.buildBranchPath(branch, filePath);
289
298
  return this.buildEntry(afsPath$1, {
290
299
  content: content$1,
291
- metadata: metadata$1,
300
+ meta: meta$1,
292
301
  createdAt: stats.birthtime,
293
302
  updatedAt: stats.mtime
294
303
  });
@@ -302,7 +311,7 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
302
311
  if (objectType === "tree") {
303
312
  const afsPath$1 = this.buildBranchPath(branch, filePath);
304
313
  const childrenCount = await this.getChildrenCount(branch, filePath);
305
- return this.buildEntry(afsPath$1, { metadata: { childrenCount } });
314
+ return this.buildEntry(afsPath$1, { meta: { childrenCount } });
306
315
  }
307
316
  const size = await this.git.raw([
308
317
  "cat-file",
@@ -312,7 +321,7 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
312
321
  const mimeType = this.getMimeType(filePath);
313
322
  const isBinary = this.isBinaryFile(filePath);
314
323
  let content;
315
- const metadata = {
324
+ const meta = {
316
325
  size,
317
326
  mimeType
318
327
  };
@@ -327,12 +336,12 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
327
336
  maxBuffer: 10 * 1024 * 1024
328
337
  });
329
338
  content = stdout.toString("base64");
330
- metadata.contentType = "base64";
339
+ meta.contentType = "base64";
331
340
  } else content = await this.git.show([`${branch}:${filePath}`]);
332
341
  const afsPath = this.buildBranchPath(branch, filePath);
333
342
  return this.buildEntry(afsPath, {
334
343
  content,
335
- metadata
344
+ meta
336
345
  });
337
346
  }
338
347
  /**
@@ -385,8 +394,8 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
385
394
  summary: payload.summary,
386
395
  createdAt: stats.birthtime,
387
396
  updatedAt: stats.mtime,
388
- metadata: {
389
- ...payload.metadata,
397
+ meta: {
398
+ ...payload.meta,
390
399
  size: stats.size
391
400
  },
392
401
  userId: payload.userId,
@@ -553,30 +562,611 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
553
562
  */
554
563
  async statRootHandler(_ctx) {
555
564
  const entry = await this.readRootHandler(_ctx);
556
- return { data: {
557
- path: entry?.path ?? "/",
558
- childrenCount: entry?.metadata?.childrenCount
559
- } };
565
+ if (!entry) return { data: void 0 };
566
+ const { content: _content, ...rest } = entry;
567
+ return { data: rest };
560
568
  }
561
569
  /**
562
570
  * Stat branch root
563
571
  */
564
572
  async statBranchRootHandler(ctx) {
565
573
  const entry = await this.readBranchRootHandler(ctx);
566
- return { data: {
567
- path: entry?.path ?? `/${ctx.params.branch}`,
568
- childrenCount: entry?.metadata?.childrenCount
569
- } };
574
+ if (!entry) return { data: void 0 };
575
+ const { content: _content, ...rest } = entry;
576
+ return { data: rest };
570
577
  }
571
578
  /**
572
579
  * Stat file or directory in branch
573
580
  */
574
581
  async statHandler(ctx) {
575
582
  const entry = await this.readBranchHandler(ctx);
576
- return { data: {
577
- path: entry?.path ?? `/${ctx.params.branch}/${ctx.params.path}`,
578
- childrenCount: entry?.metadata?.childrenCount
579
- } };
583
+ if (!entry) return { data: void 0 };
584
+ const { content: _content, ...rest } = entry;
585
+ return { data: rest };
586
+ }
587
+ /**
588
+ * Explain root → repo info, branch list, default branch
589
+ */
590
+ async explainRootHandler(_ctx) {
591
+ await this.ready();
592
+ const format = _ctx.options?.format || "markdown";
593
+ const branches = await this.getBranches();
594
+ const currentBranch = await this.git.revparse(["--abbrev-ref", "HEAD"]).then((b) => b.trim());
595
+ let remoteUrl;
596
+ try {
597
+ remoteUrl = await this.git.remote(["get-url", "origin"]).then((u) => u?.trim());
598
+ } catch {}
599
+ const lines = [];
600
+ lines.push("# Git Repository");
601
+ lines.push("");
602
+ lines.push(`**Provider:** ${this.name}`);
603
+ if (this.description) lines.push(`**Description:** ${this.description}`);
604
+ lines.push(`**Default Branch:** ${currentBranch}`);
605
+ if (remoteUrl) lines.push(`**Remote:** ${remoteUrl}`);
606
+ lines.push(`**Branches:** ${branches.length}`);
607
+ lines.push("");
608
+ lines.push("## Branches");
609
+ lines.push("");
610
+ for (const branch of branches) lines.push(`- ${branch}`);
611
+ return {
612
+ content: lines.join("\n"),
613
+ format
614
+ };
615
+ }
616
+ /**
617
+ * Explain branch → branch name, HEAD commit, file count
618
+ */
619
+ async explainBranchHandler(ctx) {
620
+ await this.ready();
621
+ const format = ctx.options?.format || "markdown";
622
+ const branch = this.decodeBranchName(ctx.params.branch);
623
+ await this.ensureBranchExists(branch);
624
+ const lastCommit = await this.getLastCommit(branch);
625
+ const fileCount = await this.getTreeFileCount(branch, "");
626
+ const lines = [];
627
+ lines.push(`# Branch: ${branch}`);
628
+ lines.push("");
629
+ lines.push(`**HEAD Commit:** ${lastCommit.shortHash} - ${lastCommit.message}`);
630
+ lines.push(`**Author:** ${lastCommit.author}`);
631
+ lines.push(`**Date:** ${lastCommit.date}`);
632
+ lines.push(`**Files:** ${fileCount} entries in tree`);
633
+ return {
634
+ content: lines.join("\n"),
635
+ format
636
+ };
637
+ }
638
+ /**
639
+ * Explain file or directory → path, size, last modified commit
640
+ */
641
+ async explainPathHandler(ctx) {
642
+ await this.ready();
643
+ const format = ctx.options?.format || "markdown";
644
+ const branch = this.decodeBranchName(ctx.params.branch);
645
+ await this.ensureBranchExists(branch);
646
+ const filePath = ctx.params.path;
647
+ const objectType = await this.git.raw([
648
+ "cat-file",
649
+ "-t",
650
+ `${branch}:${filePath}`
651
+ ]).then((t) => t.trim()).catch(() => null);
652
+ if (objectType === null) throw new AFSNotFoundError(this.buildBranchPath(branch, filePath));
653
+ const isDir = objectType === "tree";
654
+ const lines = [];
655
+ lines.push(`# ${basename(filePath)}`);
656
+ lines.push("");
657
+ lines.push(`**Path:** ${filePath}`);
658
+ lines.push(`**Type:** ${isDir ? "directory" : "file"}`);
659
+ if (!isDir) {
660
+ const size = await this.git.raw([
661
+ "cat-file",
662
+ "-s",
663
+ `${branch}:${filePath}`
664
+ ]).then((s) => Number.parseInt(s.trim(), 10));
665
+ lines.push(`**Size:** ${size} bytes`);
666
+ }
667
+ try {
668
+ const logLines = (await this.git.raw([
669
+ "log",
670
+ "-1",
671
+ "--format=%H%n%h%n%an%n%aI%n%s",
672
+ branch,
673
+ "--",
674
+ filePath
675
+ ])).trim().split("\n");
676
+ if (logLines.length >= 5) {
677
+ lines.push("");
678
+ lines.push("## Last Modified");
679
+ lines.push(`**Commit:** ${logLines[1]} - ${logLines[4]}`);
680
+ lines.push(`**Author:** ${logLines[2]}`);
681
+ lines.push(`**Date:** ${logLines[3]}`);
682
+ }
683
+ } catch {}
684
+ return {
685
+ content: lines.join("\n"),
686
+ format
687
+ };
688
+ }
689
+ async readCapabilitiesHandler(_ctx) {
690
+ const operations = [
691
+ "list",
692
+ "read",
693
+ "stat",
694
+ "explain",
695
+ "search"
696
+ ];
697
+ if (this.accessMode === "readwrite") operations.push("write", "delete", "rename");
698
+ const actionCatalogs = [];
699
+ if (this.accessMode === "readwrite") actionCatalogs.push({
700
+ description: "Git workflow actions",
701
+ catalog: [
702
+ {
703
+ name: "diff",
704
+ description: "Compare two branches or refs",
705
+ inputSchema: {
706
+ type: "object",
707
+ properties: {
708
+ from: {
709
+ type: "string",
710
+ description: "Source ref"
711
+ },
712
+ to: {
713
+ type: "string",
714
+ description: "Target ref"
715
+ },
716
+ path: {
717
+ type: "string",
718
+ description: "Optional path filter"
719
+ }
720
+ },
721
+ required: ["from", "to"]
722
+ }
723
+ },
724
+ {
725
+ name: "create-branch",
726
+ description: "Create a new branch",
727
+ inputSchema: {
728
+ type: "object",
729
+ properties: {
730
+ name: {
731
+ type: "string",
732
+ description: "New branch name"
733
+ },
734
+ from: {
735
+ type: "string",
736
+ description: "Source ref (defaults to current HEAD)"
737
+ }
738
+ },
739
+ required: ["name"]
740
+ }
741
+ },
742
+ {
743
+ name: "commit",
744
+ description: "Commit staged changes",
745
+ inputSchema: {
746
+ type: "object",
747
+ properties: {
748
+ message: {
749
+ type: "string",
750
+ description: "Commit message"
751
+ },
752
+ author: {
753
+ type: "object",
754
+ properties: {
755
+ name: { type: "string" },
756
+ email: { type: "string" }
757
+ }
758
+ }
759
+ },
760
+ required: ["message"]
761
+ }
762
+ },
763
+ {
764
+ name: "merge",
765
+ description: "Merge a branch into the current branch",
766
+ inputSchema: {
767
+ type: "object",
768
+ properties: {
769
+ branch: {
770
+ type: "string",
771
+ description: "Branch to merge"
772
+ },
773
+ message: {
774
+ type: "string",
775
+ description: "Custom merge message"
776
+ }
777
+ },
778
+ required: ["branch"]
779
+ }
780
+ }
781
+ ],
782
+ discovery: {
783
+ pathTemplate: "/:branch/.actions",
784
+ note: "Git workflow actions (readwrite mode only)"
785
+ }
786
+ });
787
+ return {
788
+ id: "/.meta/.capabilities",
789
+ path: "/.meta/.capabilities",
790
+ content: {
791
+ schemaVersion: 1,
792
+ provider: this.name,
793
+ description: this.description || "Git repository provider",
794
+ tools: [],
795
+ actions: actionCatalogs,
796
+ operations: this.getOperationsDeclaration()
797
+ },
798
+ meta: {
799
+ kind: "afs:capabilities",
800
+ operations
801
+ }
802
+ };
803
+ }
804
+ /**
805
+ * List available actions for a branch
806
+ */
807
+ async listBranchActions(ctx) {
808
+ if (this.accessMode !== "readwrite") return { data: [] };
809
+ const basePath = `/${ctx.params.branch}/.actions`;
810
+ return { data: [
811
+ {
812
+ id: "diff",
813
+ path: `${basePath}/diff`,
814
+ summary: "Compare two branches or refs",
815
+ meta: {
816
+ kind: "afs:executable",
817
+ kinds: ["afs:executable", "afs:node"],
818
+ inputSchema: {
819
+ type: "object",
820
+ properties: {
821
+ from: {
822
+ type: "string",
823
+ description: "Source ref"
824
+ },
825
+ to: {
826
+ type: "string",
827
+ description: "Target ref"
828
+ },
829
+ path: {
830
+ type: "string",
831
+ description: "Optional path filter"
832
+ }
833
+ },
834
+ required: ["from", "to"]
835
+ }
836
+ }
837
+ },
838
+ {
839
+ id: "create-branch",
840
+ path: `${basePath}/create-branch`,
841
+ summary: "Create a new branch from this ref",
842
+ meta: {
843
+ kind: "afs:executable",
844
+ kinds: ["afs:executable", "afs:node"],
845
+ inputSchema: {
846
+ type: "object",
847
+ properties: {
848
+ name: {
849
+ type: "string",
850
+ description: "New branch name"
851
+ },
852
+ from: {
853
+ type: "string",
854
+ description: "Source ref (defaults to current HEAD)"
855
+ }
856
+ },
857
+ required: ["name"]
858
+ }
859
+ }
860
+ },
861
+ {
862
+ id: "commit",
863
+ path: `${basePath}/commit`,
864
+ summary: "Commit staged changes",
865
+ meta: {
866
+ kind: "afs:executable",
867
+ kinds: ["afs:executable", "afs:node"],
868
+ inputSchema: {
869
+ type: "object",
870
+ properties: {
871
+ message: {
872
+ type: "string",
873
+ description: "Commit message"
874
+ },
875
+ author: {
876
+ type: "object",
877
+ properties: {
878
+ name: { type: "string" },
879
+ email: { type: "string" }
880
+ }
881
+ }
882
+ },
883
+ required: ["message"]
884
+ }
885
+ }
886
+ },
887
+ {
888
+ id: "merge",
889
+ path: `${basePath}/merge`,
890
+ summary: "Merge another branch into this branch",
891
+ meta: {
892
+ kind: "afs:executable",
893
+ kinds: ["afs:executable", "afs:node"],
894
+ inputSchema: {
895
+ type: "object",
896
+ properties: {
897
+ branch: {
898
+ type: "string",
899
+ description: "Branch to merge"
900
+ },
901
+ message: {
902
+ type: "string",
903
+ description: "Custom merge message"
904
+ }
905
+ },
906
+ required: ["branch"]
907
+ }
908
+ }
909
+ }
910
+ ] };
911
+ }
912
+ /**
913
+ * diff action — compare two branches or refs
914
+ */
915
+ async diffAction(_ctx, args) {
916
+ await this.ready();
917
+ const from = args.from;
918
+ const to = args.to;
919
+ const pathFilter = args.path;
920
+ if (!from || !to) return {
921
+ success: false,
922
+ error: {
923
+ code: "INVALID_ARGS",
924
+ message: "from and to are required"
925
+ }
926
+ };
927
+ try {
928
+ const diffArgs = [
929
+ "diff",
930
+ "--stat",
931
+ "--name-only",
932
+ `${from}...${to}`
933
+ ];
934
+ if (pathFilter) diffArgs.push("--", pathFilter);
935
+ const files = (await this.git.raw(diffArgs)).trim().split("\n").filter((l) => l.trim()).map((path) => ({ path }));
936
+ const patchArgs = ["diff", `${from}...${to}`];
937
+ if (pathFilter) patchArgs.push("--", pathFilter);
938
+ return {
939
+ success: true,
940
+ data: {
941
+ from,
942
+ to,
943
+ files,
944
+ patch: await this.git.raw(patchArgs),
945
+ filesChanged: files.length
946
+ }
947
+ };
948
+ } catch (error) {
949
+ return {
950
+ success: false,
951
+ error: {
952
+ code: "DIFF_FAILED",
953
+ message: error.message.replace(this.repoPath, "<repo>")
954
+ }
955
+ };
956
+ }
957
+ }
958
+ /**
959
+ * create-branch action — create a new branch
960
+ */
961
+ async createBranchAction(_ctx, args) {
962
+ await this.ready();
963
+ const name = args.name;
964
+ const from = args.from;
965
+ if (!name) return {
966
+ success: false,
967
+ error: {
968
+ code: "INVALID_ARGS",
969
+ message: "name is required"
970
+ }
971
+ };
972
+ if (name.includes("..")) return {
973
+ success: false,
974
+ error: {
975
+ code: "INVALID_NAME",
976
+ message: "Branch name contains invalid characters"
977
+ }
978
+ };
979
+ try {
980
+ if (from) await this.git.raw([
981
+ "branch",
982
+ name,
983
+ from
984
+ ]);
985
+ else await this.git.raw(["branch", name]);
986
+ return {
987
+ success: true,
988
+ data: {
989
+ branch: name,
990
+ hash: await this.git.revparse([name]).then((h) => h.trim())
991
+ }
992
+ };
993
+ } catch (error) {
994
+ return {
995
+ success: false,
996
+ error: {
997
+ code: "CREATE_BRANCH_FAILED",
998
+ message: error.message.replace(this.repoPath, "<repo>")
999
+ }
1000
+ };
1001
+ }
1002
+ }
1003
+ /**
1004
+ * commit action — commit staged changes
1005
+ */
1006
+ async commitAction(_ctx, args) {
1007
+ await this.ready();
1008
+ const message = args.message;
1009
+ if (!message) return {
1010
+ success: false,
1011
+ error: {
1012
+ code: "INVALID_ARGS",
1013
+ message: "message is required"
1014
+ }
1015
+ };
1016
+ const author = args.author;
1017
+ try {
1018
+ const git = simpleGit(this.repoPath);
1019
+ const status = await git.status();
1020
+ if (status.staged.length === 0 && status.files.filter((f) => f.index !== " " && f.index !== "?").length === 0) return {
1021
+ success: false,
1022
+ error: {
1023
+ code: "NO_CHANGES",
1024
+ message: "No staged changes to commit"
1025
+ }
1026
+ };
1027
+ if (author?.name) await git.addConfig("user.name", author.name, void 0, "local");
1028
+ if (author?.email) await git.addConfig("user.email", author.email, void 0, "local");
1029
+ const result = await git.commit(message);
1030
+ return {
1031
+ success: true,
1032
+ data: {
1033
+ hash: result.commit || "",
1034
+ message,
1035
+ filesChanged: result.summary.changes
1036
+ }
1037
+ };
1038
+ } catch (error) {
1039
+ return {
1040
+ success: false,
1041
+ error: {
1042
+ code: "COMMIT_FAILED",
1043
+ message: error.message.replace(this.repoPath, "<repo>")
1044
+ }
1045
+ };
1046
+ }
1047
+ }
1048
+ /**
1049
+ * merge action — merge a branch into current branch
1050
+ */
1051
+ async mergeAction(_ctx, args) {
1052
+ await this.ready();
1053
+ const branch = args.branch;
1054
+ if (!branch) return {
1055
+ success: false,
1056
+ error: {
1057
+ code: "INVALID_ARGS",
1058
+ message: "branch is required"
1059
+ }
1060
+ };
1061
+ const customMessage = args.message;
1062
+ try {
1063
+ const git = simpleGit(this.repoPath);
1064
+ if (!(await git.branchLocal()).all.includes(branch)) return {
1065
+ success: false,
1066
+ error: {
1067
+ code: "BRANCH_NOT_FOUND",
1068
+ message: `Branch '${branch}' not found`
1069
+ }
1070
+ };
1071
+ const mergeArgs = [branch];
1072
+ if (customMessage) mergeArgs.push("-m", customMessage);
1073
+ const result = await git.merge(mergeArgs);
1074
+ return {
1075
+ success: true,
1076
+ data: {
1077
+ hash: await git.revparse(["HEAD"]).then((h) => h.trim()),
1078
+ merged: branch,
1079
+ conflicts: result.conflicts || []
1080
+ }
1081
+ };
1082
+ } catch (error) {
1083
+ try {
1084
+ await simpleGit(this.repoPath).merge(["--abort"]);
1085
+ } catch {}
1086
+ return {
1087
+ success: false,
1088
+ error: {
1089
+ code: "MERGE_FAILED",
1090
+ message: error.message.replace(this.repoPath, "<repo>")
1091
+ }
1092
+ };
1093
+ }
1094
+ }
1095
+ /**
1096
+ * List .log/ → commit list with pagination
1097
+ */
1098
+ async listLogHandler(ctx) {
1099
+ await this.ready();
1100
+ const branch = this.decodeBranchName(ctx.params.branch);
1101
+ await this.ensureBranchExists(branch);
1102
+ const options = ctx.options;
1103
+ const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
1104
+ const offset = options?.offset || 0;
1105
+ const commits = await this.getCommitList(branch, limit, offset);
1106
+ const branchEncoded = this.encodeBranchName(branch);
1107
+ return { data: commits.map((commit, i) => this.buildEntry(`/${branchEncoded}/.log/${offset + i}`, { meta: {
1108
+ hash: commit.hash,
1109
+ shortHash: commit.shortHash,
1110
+ author: commit.author,
1111
+ date: commit.date,
1112
+ message: commit.message
1113
+ } })) };
1114
+ }
1115
+ /**
1116
+ * Read .log/{index} → commit diff/patch content
1117
+ */
1118
+ async readLogEntryHandler(ctx) {
1119
+ await this.ready();
1120
+ const branch = this.decodeBranchName(ctx.params.branch);
1121
+ await this.ensureBranchExists(branch);
1122
+ const index = Number.parseInt(ctx.params.index, 10);
1123
+ if (Number.isNaN(index) || index < 0) throw new AFSNotFoundError(`/${this.encodeBranchName(branch)}/.log/${ctx.params.index}`);
1124
+ const commits = await this.getCommitList(branch, 1, index);
1125
+ if (commits.length === 0) throw new AFSNotFoundError(`/${this.encodeBranchName(branch)}/.log/${index}`);
1126
+ const commit = commits[0];
1127
+ let diff;
1128
+ try {
1129
+ diff = await this.git.raw([
1130
+ "show",
1131
+ "--stat",
1132
+ "--patch",
1133
+ commit.hash
1134
+ ]);
1135
+ } catch {
1136
+ diff = "";
1137
+ }
1138
+ const branchEncoded = this.encodeBranchName(branch);
1139
+ return this.buildEntry(`/${branchEncoded}/.log/${index}`, {
1140
+ content: diff,
1141
+ meta: {
1142
+ hash: commit.hash,
1143
+ shortHash: commit.shortHash,
1144
+ author: commit.author,
1145
+ date: commit.date,
1146
+ message: commit.message
1147
+ }
1148
+ });
1149
+ }
1150
+ /**
1151
+ * Read .log/{index}/.meta → commit metadata only (no diff)
1152
+ */
1153
+ async readLogEntryMetaHandler(ctx) {
1154
+ await this.ready();
1155
+ const branch = this.decodeBranchName(ctx.params.branch);
1156
+ await this.ensureBranchExists(branch);
1157
+ const index = Number.parseInt(ctx.params.index, 10);
1158
+ if (Number.isNaN(index) || index < 0) throw new AFSNotFoundError(`/${this.encodeBranchName(branch)}/.log/${ctx.params.index}/.meta`);
1159
+ const commits = await this.getCommitList(branch, 1, index);
1160
+ if (commits.length === 0) throw new AFSNotFoundError(`/${this.encodeBranchName(branch)}/.log/${index}/.meta`);
1161
+ const commit = commits[0];
1162
+ const branchEncoded = this.encodeBranchName(branch);
1163
+ return this.buildEntry(`/${branchEncoded}/.log/${index}/.meta`, { meta: {
1164
+ hash: commit.hash,
1165
+ shortHash: commit.shortHash,
1166
+ author: commit.author,
1167
+ date: commit.date,
1168
+ message: commit.message
1169
+ } });
580
1170
  }
581
1171
  /**
582
1172
  * Decode branch name (replace ~ with /)
@@ -640,6 +1230,65 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
640
1230
  }
641
1231
  }
642
1232
  /**
1233
+ * Get the last commit on a branch
1234
+ */
1235
+ async getLastCommit(branch) {
1236
+ const lines = (await this.git.raw([
1237
+ "log",
1238
+ "-1",
1239
+ "--format=%H%n%h%n%an%n%aI%n%s",
1240
+ branch
1241
+ ])).trim().split("\n");
1242
+ return {
1243
+ hash: lines[0] || "",
1244
+ shortHash: lines[1] || "",
1245
+ author: lines[2] || "",
1246
+ date: lines[3] || "",
1247
+ message: lines[4] || ""
1248
+ };
1249
+ }
1250
+ /**
1251
+ * Count total files in a tree (recursively)
1252
+ */
1253
+ async getTreeFileCount(branch, path) {
1254
+ try {
1255
+ const treeish = path ? `${branch}:${path}` : branch;
1256
+ return (await this.git.raw([
1257
+ "ls-tree",
1258
+ "-r",
1259
+ treeish
1260
+ ])).split("\n").filter((line) => line.trim()).length;
1261
+ } catch {
1262
+ return 0;
1263
+ }
1264
+ }
1265
+ /**
1266
+ * Get a list of commits on a branch with limit/offset
1267
+ */
1268
+ async getCommitList(branch, limit, offset) {
1269
+ try {
1270
+ const args = [
1271
+ "log",
1272
+ `--skip=${offset}`,
1273
+ `-${limit}`,
1274
+ "--format=%H%n%h%n%an%n%aI%n%s%n---COMMIT_SEP---",
1275
+ branch
1276
+ ];
1277
+ return (await this.git.raw(args)).split("---COMMIT_SEP---").filter((b) => b.trim()).map((block) => {
1278
+ const lines = block.trim().split("\n");
1279
+ return {
1280
+ hash: lines[0] || "",
1281
+ shortHash: lines[1] || "",
1282
+ author: lines[2] || "",
1283
+ date: lines[3] || "",
1284
+ message: lines[4] || ""
1285
+ };
1286
+ });
1287
+ } catch {
1288
+ return [];
1289
+ }
1290
+ }
1291
+ /**
643
1292
  * Ensure worktree exists for a branch (lazy creation)
644
1293
  */
645
1294
  async ensureWorktree(branch) {
@@ -663,6 +1312,7 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
663
1312
  }
664
1313
  /**
665
1314
  * List files using git ls-tree (no worktree needed)
1315
+ * Note: list() returns only children, never the path itself (per new semantics)
666
1316
  */
667
1317
  async listWithGitLsTree(branch, path, options) {
668
1318
  const maxDepth = options?.maxDepth ?? 1;
@@ -677,25 +1327,8 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
677
1327
  treeish
678
1328
  ]).then((t) => t.trim()).catch(() => null);
679
1329
  if (pathType === null) throw new AFSNotFoundError(this.buildBranchPath(branch, path));
680
- if (pathType === "blob") {
681
- const size = await this.git.raw([
682
- "cat-file",
683
- "-s",
684
- treeish
685
- ]).then((s) => Number.parseInt(s.trim(), 10));
686
- const afsPath$1 = this.buildBranchPath(branch, path);
687
- entries.push(this.buildEntry(afsPath$1, { metadata: { size } }));
688
- return { data: entries };
689
- }
690
- if (maxDepth === 0) {
691
- const afsPath$1 = this.buildBranchPath(branch, path);
692
- const childrenCount$1 = await this.getChildrenCount(branch, path);
693
- entries.push(this.buildEntry(afsPath$1, { metadata: { childrenCount: childrenCount$1 } }));
694
- return { data: entries };
695
- }
696
- const afsPath = this.buildBranchPath(branch, path);
697
- const childrenCount = await this.getChildrenCount(branch, path);
698
- entries.push(this.buildEntry(afsPath, { metadata: { childrenCount } }));
1330
+ if (pathType === "blob") return { data: [] };
1331
+ if (maxDepth === 0) return { data: [] };
699
1332
  const queue = [{
700
1333
  path: targetPath,
701
1334
  depth: 0
@@ -717,11 +1350,12 @@ var AFSGit = class AFSGit extends AFSBaseProvider {
717
1350
  const isDirectory = type === "tree";
718
1351
  const size = sizeStr === "-" ? void 0 : Number.parseInt(sizeStr, 10);
719
1352
  const fullPath = itemPath ? `${itemPath}/${name}` : name;
720
- const afsPath$1 = this.buildBranchPath(branch, fullPath);
721
- const childrenCount$1 = isDirectory ? await this.getChildrenCount(branch, fullPath) : void 0;
722
- entries.push(this.buildEntry(afsPath$1, { metadata: {
1353
+ const afsPath = this.buildBranchPath(branch, fullPath);
1354
+ const childrenCount = isDirectory ? await this.getChildrenCount(branch, fullPath) : void 0;
1355
+ entries.push(this.buildEntry(afsPath, { meta: {
1356
+ kind: isDirectory ? "git:directory" : "git:file",
723
1357
  size,
724
- childrenCount: childrenCount$1
1358
+ childrenCount
725
1359
  } }));
726
1360
  if (isDirectory && depth + 1 < maxDepth) queue.push({
727
1361
  path: fullPath,
@@ -856,7 +1490,20 @@ __decorate([Search("/:branch/:path+")], AFSGit.prototype, "searchHandler", null)
856
1490
  __decorate([Stat("/")], AFSGit.prototype, "statRootHandler", null);
857
1491
  __decorate([Stat("/:branch")], AFSGit.prototype, "statBranchRootHandler", null);
858
1492
  __decorate([Stat("/:branch/:path+")], AFSGit.prototype, "statHandler", null);
1493
+ __decorate([Explain("/")], AFSGit.prototype, "explainRootHandler", null);
1494
+ __decorate([Explain("/:branch")], AFSGit.prototype, "explainBranchHandler", null);
1495
+ __decorate([Explain("/:branch/:path+")], AFSGit.prototype, "explainPathHandler", null);
1496
+ __decorate([Read("/.meta/.capabilities")], AFSGit.prototype, "readCapabilitiesHandler", null);
1497
+ __decorate([Actions("/:branch")], AFSGit.prototype, "listBranchActions", null);
1498
+ __decorate([Actions.Exec("/:branch", "diff")], AFSGit.prototype, "diffAction", null);
1499
+ __decorate([Actions.Exec("/:branch", "create-branch")], AFSGit.prototype, "createBranchAction", null);
1500
+ __decorate([Actions.Exec("/:branch", "commit")], AFSGit.prototype, "commitAction", null);
1501
+ __decorate([Actions.Exec("/:branch", "merge")], AFSGit.prototype, "mergeAction", null);
1502
+ __decorate([List("/:branch/.log")], AFSGit.prototype, "listLogHandler", null);
1503
+ __decorate([Read("/:branch/.log/:index")], AFSGit.prototype, "readLogEntryHandler", null);
1504
+ __decorate([Read("/:branch/.log/:index/.meta")], AFSGit.prototype, "readLogEntryMetaHandler", null);
1505
+ var src_default = AFSGit;
859
1506
 
860
1507
  //#endregion
861
- export { AFSGit };
1508
+ export { AFSGit, src_default as default };
862
1509
  //# sourceMappingURL=index.mjs.map