@devframes/plugin-git 0.0.1

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 (37) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +94 -0
  3. package/bin.mjs +3 -0
  4. package/dist/cli.d.mts +1 -0
  5. package/dist/cli.mjs +12 -0
  6. package/dist/client/404/index.html +1 -0
  7. package/dist/client/404.html +1 -0
  8. package/dist/client/__next.__PAGE__.txt +6 -0
  9. package/dist/client/__next._full.txt +14 -0
  10. package/dist/client/__next._head.txt +5 -0
  11. package/dist/client/__next._index.txt +5 -0
  12. package/dist/client/__next._tree.txt +2 -0
  13. package/dist/client/_next/static/chunks/02~fscl7ckl8~.js +5 -0
  14. package/dist/client/_next/static/chunks/03~yq9q893hmn.js +1 -0
  15. package/dist/client/_next/static/chunks/0cby-xtlytqc8.js +31 -0
  16. package/dist/client/_next/static/chunks/0e1~n36m7gfkb.js +1 -0
  17. package/dist/client/_next/static/chunks/0f-3h51aw8u6x.js +1 -0
  18. package/dist/client/_next/static/chunks/0fh_ktutzk5fe.js +1 -0
  19. package/dist/client/_next/static/chunks/0o2sgd3e~ptl1.css +8 -0
  20. package/dist/client/_next/static/chunks/turbopack-0_ztfbn7~a1nl.js +1 -0
  21. package/dist/client/_next/static/hfdjoy_Phicjdm2uzC1Xt/_buildManifest.js +16 -0
  22. package/dist/client/_next/static/hfdjoy_Phicjdm2uzC1Xt/_clientMiddlewareManifest.js +1 -0
  23. package/dist/client/_next/static/hfdjoy_Phicjdm2uzC1Xt/_ssgManifest.js +1 -0
  24. package/dist/client/_not-found/__next._full.txt +15 -0
  25. package/dist/client/_not-found/__next._head.txt +5 -0
  26. package/dist/client/_not-found/__next._index.txt +5 -0
  27. package/dist/client/_not-found/__next._not-found.__PAGE__.txt +5 -0
  28. package/dist/client/_not-found/__next._not-found.txt +5 -0
  29. package/dist/client/_not-found/__next._tree.txt +2 -0
  30. package/dist/client/_not-found/index.html +1 -0
  31. package/dist/client/_not-found/index.txt +15 -0
  32. package/dist/client/index.html +1 -0
  33. package/dist/client/index.txt +14 -0
  34. package/dist/index.d.mts +205 -0
  35. package/dist/index.mjs +2 -0
  36. package/dist/src-Bm_Zrtej.mjs +756 -0
  37. package/package.json +80 -0
@@ -0,0 +1,756 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { defineDevframe } from "devframe/types";
3
+ import { dirname, resolve } from "pathe";
4
+ import { execFile } from "node:child_process";
5
+ import process from "node:process";
6
+ import { promisify } from "node:util";
7
+ import { defineRpcFunction } from "devframe";
8
+ //#region package.json
9
+ var name = "@devframes/plugin-git";
10
+ var version = "0.0.1";
11
+ var description = "Git integration for devframe — a read-only repository dashboard (status, log, branches, diff) with a Next.js + shadcn/ui SPA over type-safe RPC.";
12
+ var homepage = "https://github.com/devframes/devframe#readme";
13
+ //#endregion
14
+ //#region src/node/git.ts
15
+ const execFileAsync = promisify(execFile);
16
+ const MAX_BUFFER = 1024 * 1024 * 64;
17
+ /**
18
+ * Run a git command in `cwd`. Rejects when git exits non-zero — callers that
19
+ * tolerate failure (e.g. "no upstream configured") should use {@link tryGit}.
20
+ */
21
+ async function runGit(cwd, args) {
22
+ const { stdout, stderr } = await execFileAsync("git", args, {
23
+ cwd,
24
+ maxBuffer: MAX_BUFFER,
25
+ windowsHide: true,
26
+ env: {
27
+ ...process.env,
28
+ GIT_PAGER: "cat",
29
+ GIT_OPTIONAL_LOCKS: "0",
30
+ LC_ALL: "C"
31
+ }
32
+ });
33
+ return {
34
+ stdout,
35
+ stderr
36
+ };
37
+ }
38
+ /** Run a git command, returning trimmed stdout or `null` when it fails. */
39
+ async function tryGit(cwd, args) {
40
+ try {
41
+ const { stdout } = await runGit(cwd, args);
42
+ return stdout.replace(/\n$/, "");
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ /** Resolve the repository root for `cwd`, or `null` when `cwd` is outside a repo. */
48
+ async function resolveRepoRoot(cwd) {
49
+ return tryGit(cwd, ["rev-parse", "--show-toplevel"]);
50
+ }
51
+ /** Split git output on a separator, dropping the trailing empty segment. */
52
+ function splitClean(input, separator) {
53
+ return input.split(separator).filter((part) => part.length > 0);
54
+ }
55
+ /**
56
+ * Extract a concise, single-line message from a failed `execFile` error.
57
+ * git writes useful text (e.g. "nothing to commit") to stdout/stderr.
58
+ */
59
+ function gitErrorMessage(error) {
60
+ const e = error;
61
+ return (e?.stderr || e?.stdout || e?.message || "git command failed").trim().split("\n")[0];
62
+ }
63
+ //#endregion
64
+ //#region src/rpc/context.ts
65
+ const configs = /* @__PURE__ */ new WeakMap();
66
+ const contexts = /* @__PURE__ */ new WeakMap();
67
+ /**
68
+ * Record the working directory for a context. Called from the devframe
69
+ * `setup` before any RPC handler runs, so {@link getGitContext} can honor a
70
+ * `repoRoot` override instead of the raw `ctx.cwd`.
71
+ */
72
+ function configureGit(ctx, config) {
73
+ configs.set(ctx, config);
74
+ }
75
+ /**
76
+ * Per-`DevframeNodeContext` git state. Each RPC function file pulls its
77
+ * working directory and (memoized) repo-root lookup from here instead of
78
+ * re-resolving on every call.
79
+ */
80
+ function getGitContext(ctx) {
81
+ let existing = contexts.get(ctx);
82
+ if (existing) return existing;
83
+ const config = configs.get(ctx);
84
+ const cwd = config?.cwd ?? ctx.cwd;
85
+ const write = config?.write ?? false;
86
+ let rootPromise;
87
+ existing = {
88
+ cwd,
89
+ write,
90
+ resolveRoot: () => {
91
+ rootPromise ??= resolveRepoRoot(cwd);
92
+ return rootPromise;
93
+ }
94
+ };
95
+ contexts.set(ctx, existing);
96
+ return existing;
97
+ }
98
+ //#endregion
99
+ //#region src/rpc/functions/branches.ts
100
+ const FORMAT$1 = [
101
+ "%(refname:short)",
102
+ "%(objectname:short)",
103
+ "%(HEAD)",
104
+ "%(upstream:short)",
105
+ "%(upstream:track)",
106
+ "%(contents:subject)"
107
+ ].join("");
108
+ function parseTrack(track) {
109
+ if (track.includes("gone")) return {
110
+ ahead: 0,
111
+ behind: 0,
112
+ gone: true
113
+ };
114
+ const ahead = track.match(/ahead (\d+)/);
115
+ const behind = track.match(/behind (\d+)/);
116
+ return {
117
+ ahead: ahead ? Number(ahead[1]) : 0,
118
+ behind: behind ? Number(behind[1]) : 0,
119
+ gone: false
120
+ };
121
+ }
122
+ const branches = defineRpcFunction({
123
+ name: "git:branches",
124
+ type: "query",
125
+ snapshot: true,
126
+ jsonSerializable: true,
127
+ setup: (ctx) => {
128
+ const git = getGitContext(ctx);
129
+ return { handler: async () => {
130
+ if (!await git.resolveRoot()) return {
131
+ isRepo: false,
132
+ current: null,
133
+ branches: []
134
+ };
135
+ const raw = await tryGit(git.cwd, [
136
+ "for-each-ref",
137
+ `--format=${FORMAT$1}`,
138
+ "refs/heads"
139
+ ]);
140
+ if (!raw) return {
141
+ isRepo: true,
142
+ current: null,
143
+ branches: []
144
+ };
145
+ let current = null;
146
+ const branches = splitClean(raw, "\n").map((line) => {
147
+ const [name, sha, head, upstream, track, subject] = line.split("");
148
+ const isCurrent = head === "*";
149
+ if (isCurrent) current = name;
150
+ return {
151
+ name,
152
+ current: isCurrent,
153
+ sha,
154
+ upstream: upstream || null,
155
+ subject: subject ?? "",
156
+ ...parseTrack(track ?? "")
157
+ };
158
+ });
159
+ branches.sort((a, b) => Number(b.current) - Number(a.current));
160
+ return {
161
+ isRepo: true,
162
+ current,
163
+ branches
164
+ };
165
+ } };
166
+ }
167
+ });
168
+ //#endregion
169
+ //#region src/rpc/functions/status.ts
170
+ const EMPTY_STATUS = {
171
+ isRepo: false,
172
+ root: null,
173
+ branch: null,
174
+ detached: false,
175
+ head: null,
176
+ upstream: null,
177
+ ahead: 0,
178
+ behind: 0,
179
+ staged: [],
180
+ unstaged: [],
181
+ untracked: [],
182
+ clean: true,
183
+ canWrite: false
184
+ };
185
+ function mapCode(code) {
186
+ switch (code) {
187
+ case "M": return "modified";
188
+ case "A": return "added";
189
+ case "D": return "deleted";
190
+ case "R": return "renamed";
191
+ case "C": return "copied";
192
+ case "T": return "type-changed";
193
+ case "U": return "unmerged";
194
+ default: return "unknown";
195
+ }
196
+ }
197
+ /**
198
+ * Parse `git status --porcelain=v2 --branch -z` into a structured snapshot.
199
+ * Records are NUL-separated; rename/copy (type `2`) entries consume an extra
200
+ * token for the original path.
201
+ */
202
+ function parseStatus(root, raw) {
203
+ const tokens = raw.split("\0");
204
+ const status = {
205
+ ...EMPTY_STATUS,
206
+ isRepo: true,
207
+ root,
208
+ staged: [],
209
+ unstaged: [],
210
+ untracked: []
211
+ };
212
+ for (let i = 0; i < tokens.length; i++) {
213
+ const token = tokens[i];
214
+ if (!token) continue;
215
+ if (token.startsWith("# ")) {
216
+ const [, key, ...rest] = token.split(" ");
217
+ const value = rest.join(" ");
218
+ if (key === "branch.head") if (value === "(detached)") {
219
+ status.detached = true;
220
+ status.branch = null;
221
+ } else status.branch = value;
222
+ else if (key === "branch.oid" && value !== "(initial)") status.head = value.slice(0, 9);
223
+ else if (key === "branch.upstream") status.upstream = value;
224
+ else if (key === "branch.ab") {
225
+ const match = value.match(/\+(\d+)\s+-(\d+)/);
226
+ if (match) {
227
+ status.ahead = Number(match[1]);
228
+ status.behind = Number(match[2]);
229
+ }
230
+ }
231
+ continue;
232
+ }
233
+ if (token.startsWith("1 ") || token.startsWith("2 ")) {
234
+ const renamed = token.startsWith("2 ");
235
+ const fields = token.split(" ");
236
+ const xy = fields[1];
237
+ const x = xy[0];
238
+ const y = xy[1];
239
+ const path = fields.slice(renamed ? 9 : 8).join(" ");
240
+ const from = renamed ? tokens[++i] : void 0;
241
+ if (x !== ".") status.staged.push(from ? {
242
+ path,
243
+ from,
244
+ status: mapCode(x)
245
+ } : {
246
+ path,
247
+ status: mapCode(x)
248
+ });
249
+ if (y !== ".") status.unstaged.push({
250
+ path,
251
+ status: mapCode(y)
252
+ });
253
+ continue;
254
+ }
255
+ if (token.startsWith("u ")) {
256
+ const path = token.split(" ").slice(10).join(" ");
257
+ status.unstaged.push({
258
+ path,
259
+ status: "unmerged"
260
+ });
261
+ continue;
262
+ }
263
+ if (token.startsWith("? ")) status.untracked.push(token.slice(2));
264
+ }
265
+ status.clean = status.staged.length === 0 && status.unstaged.length === 0 && status.untracked.length === 0;
266
+ return status;
267
+ }
268
+ /**
269
+ * Read the working-tree status for a git context. Shared by the `git:status`
270
+ * query and the write actions (which return fresh status after mutating).
271
+ */
272
+ async function readStatus(git) {
273
+ const root = await git.resolveRoot();
274
+ if (!root) return {
275
+ ...EMPTY_STATUS,
276
+ canWrite: false
277
+ };
278
+ const { stdout } = await runGit(git.cwd, [
279
+ "status",
280
+ "--porcelain=v2",
281
+ "--branch",
282
+ "-z"
283
+ ]);
284
+ const status = parseStatus(root, stdout);
285
+ status.canWrite = git.write;
286
+ return status;
287
+ }
288
+ const status = defineRpcFunction({
289
+ name: "git:status",
290
+ type: "query",
291
+ snapshot: true,
292
+ jsonSerializable: true,
293
+ setup: (ctx) => {
294
+ const git = getGitContext(ctx);
295
+ return { handler: () => readStatus(git) };
296
+ }
297
+ });
298
+ //#endregion
299
+ //#region src/rpc/functions/commit.ts
300
+ const commit = defineRpcFunction({
301
+ name: "git:commit",
302
+ type: "action",
303
+ jsonSerializable: true,
304
+ setup: (ctx) => {
305
+ const git = getGitContext(ctx);
306
+ return { handler: async (args) => {
307
+ const message = (args?.message ?? "").trim();
308
+ if (!await git.resolveRoot()) return {
309
+ ok: false,
310
+ hash: null,
311
+ message: "Not a git repository.",
312
+ status: await readStatus(git)
313
+ };
314
+ if (!message) return {
315
+ ok: false,
316
+ hash: null,
317
+ message: "Commit message is required.",
318
+ status: await readStatus(git)
319
+ };
320
+ try {
321
+ await runGit(git.cwd, [
322
+ "commit",
323
+ "-m",
324
+ message
325
+ ]);
326
+ return {
327
+ ok: true,
328
+ hash: await tryGit(git.cwd, [
329
+ "rev-parse",
330
+ "--short",
331
+ "HEAD"
332
+ ]),
333
+ message: "Committed.",
334
+ status: await readStatus(git)
335
+ };
336
+ } catch (error) {
337
+ return {
338
+ ok: false,
339
+ hash: null,
340
+ message: gitErrorMessage(error),
341
+ status: await readStatus(git)
342
+ };
343
+ }
344
+ } };
345
+ }
346
+ });
347
+ //#endregion
348
+ //#region src/rpc/functions/diff.ts
349
+ /** Hard cap on the returned patch text to keep payloads bounded. */
350
+ const PATCH_CHAR_LIMIT$1 = 2e5;
351
+ function parseNumstat$1(raw) {
352
+ return splitClean(raw, "\n").map((line) => {
353
+ const [add, del, ...rest] = line.split(" ");
354
+ const binary = add === "-" || del === "-";
355
+ return {
356
+ path: rest.join(" "),
357
+ additions: binary ? 0 : Number(add),
358
+ deletions: binary ? 0 : Number(del),
359
+ binary
360
+ };
361
+ });
362
+ }
363
+ const diff = defineRpcFunction({
364
+ name: "git:diff",
365
+ type: "query",
366
+ snapshot: true,
367
+ jsonSerializable: true,
368
+ setup: (ctx) => {
369
+ const git = getGitContext(ctx);
370
+ return { handler: async (args = {}) => {
371
+ const { path, staged = false } = args;
372
+ if (!await git.resolveRoot()) return {
373
+ isRepo: false,
374
+ staged,
375
+ path: path ?? null,
376
+ files: [],
377
+ totalAdditions: 0,
378
+ totalDeletions: 0,
379
+ patch: null,
380
+ truncated: false
381
+ };
382
+ const base = staged ? ["diff", "--cached"] : ["diff"];
383
+ const scope = path ? ["--", path] : [];
384
+ const numstatRaw = await tryGit(git.cwd, [
385
+ ...base,
386
+ "--numstat",
387
+ ...scope
388
+ ]);
389
+ const files = numstatRaw ? parseNumstat$1(numstatRaw) : [];
390
+ const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0);
391
+ const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0);
392
+ let patch = null;
393
+ let truncated = false;
394
+ if (path) {
395
+ const { stdout } = await runGit(git.cwd, [...base, ...scope]);
396
+ if (stdout.length > PATCH_CHAR_LIMIT$1) {
397
+ patch = stdout.slice(0, PATCH_CHAR_LIMIT$1);
398
+ truncated = true;
399
+ } else patch = stdout;
400
+ }
401
+ return {
402
+ isRepo: true,
403
+ staged,
404
+ path: path ?? null,
405
+ files,
406
+ totalAdditions,
407
+ totalDeletions,
408
+ patch,
409
+ truncated
410
+ };
411
+ } };
412
+ }
413
+ });
414
+ //#endregion
415
+ //#region src/rpc/functions/log.ts
416
+ const FORMAT = [
417
+ "%H",
418
+ "%h",
419
+ "%P",
420
+ "%an",
421
+ "%ae",
422
+ "%aI",
423
+ "%D",
424
+ "%s",
425
+ "%b"
426
+ ].join("") + "";
427
+ function clamp(value, min, max) {
428
+ return Math.min(Math.max(value, min), max);
429
+ }
430
+ function parseLog(raw) {
431
+ return splitClean(raw, "").map((record) => {
432
+ const [hash, shortHash, parents, author, email, isoDate, refs, subject, body] = record.replace(/^\n/, "").split("");
433
+ return {
434
+ hash,
435
+ shortHash,
436
+ author,
437
+ email,
438
+ date: Date.parse(isoDate),
439
+ subject,
440
+ body: (body ?? "").trim(),
441
+ refs: refs ? refs.split(", ").map((r) => r.trim()).filter(Boolean) : [],
442
+ parents: parents ? parents.split(" ").filter(Boolean) : []
443
+ };
444
+ });
445
+ }
446
+ const SNAPSHOT_LIMIT$1 = 200;
447
+ const log = defineRpcFunction({
448
+ name: "git:log",
449
+ type: "query",
450
+ jsonSerializable: true,
451
+ dump: async (_ctx, handler) => {
452
+ const baked = {
453
+ ...await handler({
454
+ limit: SNAPSHOT_LIMIT$1,
455
+ skip: 0
456
+ }),
457
+ hasMore: false
458
+ };
459
+ return {
460
+ records: [{
461
+ inputs: [],
462
+ output: baked
463
+ }],
464
+ fallback: baked
465
+ };
466
+ },
467
+ setup: (ctx) => {
468
+ const git = getGitContext(ctx);
469
+ return { handler: async (args = {}) => {
470
+ const limit = clamp(Math.trunc(args.limit ?? 30), 1, 200);
471
+ const skip = Math.max(0, Math.trunc(args.skip ?? 0));
472
+ const ref = args.ref?.trim() || void 0;
473
+ if (!await git.resolveRoot()) return {
474
+ isRepo: false,
475
+ commits: [],
476
+ limit,
477
+ skip,
478
+ hasMore: false
479
+ };
480
+ const command = [
481
+ "log",
482
+ "--topo-order",
483
+ `--max-count=${limit}`,
484
+ `--skip=${skip}`,
485
+ `--pretty=format:${FORMAT}`
486
+ ];
487
+ if (ref) command.push(ref);
488
+ const raw = await tryGit(git.cwd, command);
489
+ const commits = raw ? parseLog(raw) : [];
490
+ return {
491
+ isRepo: true,
492
+ commits,
493
+ limit,
494
+ skip,
495
+ hasMore: commits.length === limit
496
+ };
497
+ } };
498
+ }
499
+ });
500
+ //#endregion
501
+ //#region src/rpc/functions/show.ts
502
+ /** Hard cap on the returned patch text to keep payloads bounded. */
503
+ const PATCH_CHAR_LIMIT = 2e5;
504
+ /** Matches the window `git:log` bakes, so any visible commit has a snapshot. */
505
+ const SNAPSHOT_LIMIT = 200;
506
+ const EMPTY_DETAIL = {
507
+ isRepo: false,
508
+ found: false,
509
+ hash: "",
510
+ shortHash: "",
511
+ author: "",
512
+ email: "",
513
+ date: 0,
514
+ committer: "",
515
+ committerEmail: "",
516
+ commitDate: 0,
517
+ subject: "",
518
+ body: "",
519
+ parents: [],
520
+ refs: [],
521
+ files: [],
522
+ totalAdditions: 0,
523
+ totalDeletions: 0,
524
+ patch: null,
525
+ truncated: false
526
+ };
527
+ const SHOW_FORMAT = [
528
+ "%H",
529
+ "%h",
530
+ "%P",
531
+ "%an",
532
+ "%ae",
533
+ "%aI",
534
+ "%cn",
535
+ "%ce",
536
+ "%cI",
537
+ "%D",
538
+ "%s",
539
+ "%b"
540
+ ].join("");
541
+ function parseNumstat(raw) {
542
+ return splitClean(raw, "\n").map((line) => {
543
+ const [add, del, ...rest] = line.split(" ");
544
+ const binary = add === "-" || del === "-";
545
+ return {
546
+ path: rest.join(" "),
547
+ additions: binary ? 0 : Number(add),
548
+ deletions: binary ? 0 : Number(del),
549
+ binary
550
+ };
551
+ });
552
+ }
553
+ async function readCommit(git, hash, includePatch) {
554
+ const meta = await tryGit(git.cwd, [
555
+ "show",
556
+ "-s",
557
+ `--format=${SHOW_FORMAT}`,
558
+ hash
559
+ ]);
560
+ if (meta == null) return {
561
+ ...EMPTY_DETAIL,
562
+ isRepo: true
563
+ };
564
+ const [fullHash, shortHash, parents, author, email, authorDate, committer, committerEmail, committerDate, refs, subject, body] = meta.split("");
565
+ const numstat = await tryGit(git.cwd, [
566
+ "diff-tree",
567
+ "--no-commit-id",
568
+ "--numstat",
569
+ "-r",
570
+ "--root",
571
+ hash
572
+ ]);
573
+ const files = numstat ? parseNumstat(numstat) : [];
574
+ const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0);
575
+ const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0);
576
+ let patch = null;
577
+ let truncated = false;
578
+ if (includePatch) {
579
+ const raw = await tryGit(git.cwd, [
580
+ "diff-tree",
581
+ "-p",
582
+ "--no-commit-id",
583
+ "-r",
584
+ "--root",
585
+ hash
586
+ ]);
587
+ if (raw != null) if (raw.length > PATCH_CHAR_LIMIT) {
588
+ patch = raw.slice(0, PATCH_CHAR_LIMIT);
589
+ truncated = true;
590
+ } else patch = raw;
591
+ }
592
+ return {
593
+ isRepo: true,
594
+ found: true,
595
+ hash: fullHash,
596
+ shortHash,
597
+ author,
598
+ email,
599
+ date: Date.parse(authorDate),
600
+ committer,
601
+ committerEmail,
602
+ commitDate: Date.parse(committerDate),
603
+ subject,
604
+ body: (body ?? "").trim(),
605
+ parents: parents ? parents.split(" ").filter(Boolean) : [],
606
+ refs: refs ? refs.split(", ").map((r) => r.trim()).filter(Boolean) : [],
607
+ files,
608
+ totalAdditions,
609
+ totalDeletions,
610
+ patch,
611
+ truncated
612
+ };
613
+ }
614
+ const show = defineRpcFunction({
615
+ name: "git:show",
616
+ type: "query",
617
+ jsonSerializable: true,
618
+ dump: async (ctx, _handler) => {
619
+ const git = getGitContext(ctx);
620
+ if (!await git.resolveRoot()) return {
621
+ records: [],
622
+ fallback: EMPTY_DETAIL
623
+ };
624
+ const raw = await tryGit(git.cwd, [
625
+ "log",
626
+ "--topo-order",
627
+ `--max-count=${SNAPSHOT_LIMIT}`,
628
+ "--pretty=format:%H"
629
+ ]);
630
+ const hashes = raw ? raw.split("\n").filter(Boolean) : [];
631
+ const records = [];
632
+ for (const hash of hashes) {
633
+ const output = await readCommit(git, hash, false);
634
+ records.push({
635
+ inputs: [{ hash }],
636
+ output
637
+ });
638
+ }
639
+ return {
640
+ records,
641
+ fallback: records[0]?.output ?? {
642
+ ...EMPTY_DETAIL,
643
+ isRepo: true
644
+ }
645
+ };
646
+ },
647
+ setup: (ctx) => {
648
+ const git = getGitContext(ctx);
649
+ return { handler: async (args) => {
650
+ const hash = (args?.hash ?? "").trim();
651
+ const includePatch = args?.patch ?? true;
652
+ if (!await git.resolveRoot() || !hash) return EMPTY_DETAIL;
653
+ return readCommit(git, hash, includePatch);
654
+ } };
655
+ }
656
+ });
657
+ //#endregion
658
+ //#region src/rpc/functions/stage.ts
659
+ const stage = defineRpcFunction({
660
+ name: "git:stage",
661
+ type: "action",
662
+ jsonSerializable: true,
663
+ setup: (ctx) => {
664
+ const git = getGitContext(ctx);
665
+ return { handler: async (args) => {
666
+ const paths = args?.paths ?? [];
667
+ if (await git.resolveRoot() && paths.length > 0) await runGit(git.cwd, [
668
+ "add",
669
+ "--",
670
+ ...paths
671
+ ]);
672
+ return readStatus(git);
673
+ } };
674
+ }
675
+ });
676
+ //#endregion
677
+ //#region src/rpc/functions/unstage.ts
678
+ const unstage = defineRpcFunction({
679
+ name: "git:unstage",
680
+ type: "action",
681
+ jsonSerializable: true,
682
+ setup: (ctx) => {
683
+ const git = getGitContext(ctx);
684
+ return { handler: async (args) => {
685
+ const paths = args?.paths ?? [];
686
+ if (await git.resolveRoot() && paths.length > 0) await runGit(git.cwd, [
687
+ "restore",
688
+ "--staged",
689
+ "--",
690
+ ...paths
691
+ ]);
692
+ return readStatus(git);
693
+ } };
694
+ }
695
+ });
696
+ //#endregion
697
+ //#region src/rpc/index.ts
698
+ /** Read-only RPC — always registered. */
699
+ const readFunctions = [
700
+ status,
701
+ log,
702
+ show,
703
+ branches,
704
+ diff
705
+ ];
706
+ /** Mutating RPC — registered only when write actions are enabled. */
707
+ const writeFunctions = [
708
+ stage,
709
+ unstage,
710
+ commit
711
+ ];
712
+ //#endregion
713
+ //#region src/index.ts
714
+ const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
715
+ /**
716
+ * Create the Git dashboard devframe. Mount it into any host via devframe's
717
+ * adapters, or run it standalone with the bundled CLI (`devframe-git`).
718
+ *
719
+ * @experimental This plugin is experimental and may change without a major
720
+ * version bump until it stabilizes.
721
+ */
722
+ function createGitDevframe(options = {}) {
723
+ const distDir = options.distDir ?? resolve(PKG_ROOT, "dist/client");
724
+ return defineDevframe({
725
+ id: "git",
726
+ name: "Git",
727
+ version,
728
+ packageName: name,
729
+ homepage,
730
+ description,
731
+ icon: "ph:git-branch-duotone",
732
+ basePath: options.basePath,
733
+ cli: {
734
+ command: "devframe-git",
735
+ port: options.port ?? 9710,
736
+ distDir,
737
+ auth: false,
738
+ configure(cli) {
739
+ cli.option("--write", "Enable staging, unstaging, and committing from the UI");
740
+ }
741
+ },
742
+ spa: { loader: "none" },
743
+ setup(ctx, info) {
744
+ const write = options.write ?? info?.flags?.write === true;
745
+ configureGit(ctx, {
746
+ cwd: options.repoRoot ? resolve(options.repoRoot) : ctx.cwd,
747
+ write
748
+ });
749
+ for (const fn of readFunctions) ctx.rpc.register(fn);
750
+ if (write) for (const fn of writeFunctions) ctx.rpc.register(fn);
751
+ }
752
+ });
753
+ }
754
+ var src_default = createGitDevframe();
755
+ //#endregion
756
+ export { src_default as n, createGitDevframe as t };