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