@devframes/plugin-git 0.9.1 → 0.9.3

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/README.md CHANGED
@@ -9,9 +9,10 @@ repository dashboard with a **Next.js App Router + shadcn/ui** SPA over
9
9
  type-safe RPC. The host process shells out to `git` and exposes the repository;
10
10
  the same bundle runs as a live dev server or a fully static deployment.
11
11
 
12
- Status, a SourceTree-style **commit graph**, branches, and diffs are read-only;
13
- staging, unstaging, and committing are available when write mode is enabled. The
14
- UI follows the system **light/dark** preference with a manual toggle.
12
+ Status, a SourceTree-style **commit graph**, branches, and diffs, plus staging,
13
+ unstaging, and committing — all through the shared
14
+ [`@devframes/service-git`](../../services/git) wire service. The UI follows the
15
+ system **light/dark** preference with a manual toggle.
15
16
 
16
17
  ## Install
17
18
 
@@ -25,7 +26,6 @@ Run the dashboard against the current repository:
25
26
 
26
27
  ```sh
27
28
  pnpx @devframes/plugin-git # dev server (live RPC over WebSocket)
28
- pnpx @devframes/plugin-git --write # also enable staging / committing from the UI
29
29
  pnpx @devframes/plugin-git build # static deploy → dist-static/
30
30
  pnpx @devframes/plugin-git --port 4000
31
31
  ```
@@ -48,30 +48,32 @@ await createCac(createGitDevframe({ repoRoot: process.cwd() })).parse()
48
48
  | `basePath` | adapter-resolved | Mount path (`/` standalone, `/__git/` hosted). |
49
49
  | `distDir` | bundled SPA | Override the served SPA directory. |
50
50
  | `port` | `9710` | Preferred dev-server port. |
51
- | `write` | `false` | Enable staging, unstaging, and committing from the UI. |
52
51
 
53
52
  ## RPC surface
54
53
 
55
- The read functions are each a `query` with `snapshot: true`: resolved live over
56
- WebSocket in dev, and served from a snapshot baked at build time for static
57
- deploys. Each degrades to an empty, `isRepo: false` result outside a git
58
- repository.
59
-
60
- - `devframes:plugin:git:status` — branch, upstream tracking (ahead/behind), staged / unstaged /
61
- untracked files, parsed from `git status --porcelain=v2`. Reports `canWrite`.
62
- - `devframes:plugin:git:log` — paginated commit history (`limit` / `skip`) including parent
54
+ All git work runs through the [`@devframes/service-git`](../../services/git)
55
+ wire service, which this devframe declares (`services`) and its SPA calls
56
+ directly over `devframes:service:git:*`. The read functions are `query`
57
+ functions that degrade to an empty, `isRepo: false` result outside a git
58
+ repository; the definition opts them into the static build via `rpc.snapshot`
59
+ (resolved live over WebSocket in dev, served from a build-time snapshot for
60
+ static deploys).
61
+
62
+ - `devframes:service:git:status` — branch, upstream tracking (ahead/behind), staged / unstaged /
63
+ untracked files, parsed from `git status --porcelain=v2`.
64
+ - `devframes:service:git:log` — paginated commit history (`limit` / `skip`) including parent
63
65
  hashes, which drive the commit graph.
64
- - `devframes:plugin:git:branches` — local branches with SHA, upstream, ahead/behind, tip subject.
65
- - `devframes:plugin:git:diff` — per-file added/deleted counts for the working tree or index, plus
66
+ - `devframes:service:git:branches` — local branches with SHA, upstream, ahead/behind, tip subject.
67
+ - `devframes:service:git:diff` — per-file added/deleted counts for the working tree or index, plus
66
68
  a unified patch for a selected file.
67
69
 
68
- Write actions are `action` functions, registered only when write mode is enabled
69
- (`createGitDevframe({ write: true })` or the `--write` flag) and gated behind
70
- `status.canWrite` in the UI. Each returns fresh status (commit returns a result):
70
+ Write actions are `action` functions — always exposed by the service, with
71
+ write authorization governed by the host's connection-trust boundary. Each
72
+ returns fresh status (commit returns a result):
71
73
 
72
- - `devframes:plugin:git:stage` — `git add` the given paths.
73
- - `devframes:plugin:git:unstage` — `git restore --staged` the given paths.
74
- - `devframes:plugin:git:commit` — commit the staged changes with a message.
74
+ - `devframes:service:git:stage` — `git add` the given paths.
75
+ - `devframes:service:git:unstage` — `git restore --staged` the given paths.
76
+ - `devframes:service:git:commit` — commit the staged changes with a message.
75
77
 
76
78
  ## Develop
77
79
 
package/dist/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as createGitDevframe } from "./src-CyRmAQrb.mjs";
1
+ import { t as createGitDevframe } from "./src-E7rY0DjA.mjs";
2
2
  import process from "node:process";
3
3
  import { createCac } from "devframe/adapters/cac";
4
4
  //#region src/cli.ts
package/dist/index.d.mts CHANGED
@@ -1,180 +1,4 @@
1
1
  import { DevframeDefinition } from "devframe";
2
- //#region src/rpc/functions/branches.d.ts
3
- interface Branch {
4
- name: string;
5
- current: boolean;
6
- sha: string;
7
- upstream: string | null;
8
- subject: string;
9
- ahead: number;
10
- behind: number;
11
- /** `true` when the upstream branch no longer exists. */
12
- gone: boolean;
13
- }
14
- interface GitBranches {
15
- isRepo: boolean;
16
- current: string | null;
17
- branches: Branch[];
18
- }
19
- //#endregion
20
- //#region src/rpc/functions/status.d.ts
21
- type FileStatusCode = 'modified' | 'added' | 'deleted' | 'renamed' | 'copied' | 'type-changed' | 'unmerged' | 'unknown';
22
- interface StatusFileEntry {
23
- path: string;
24
- /** Previous path, present for renames and copies. */
25
- from?: string;
26
- status: FileStatusCode;
27
- }
28
- interface GitStatus {
29
- /** `false` when the working directory is not inside a git repository. */
30
- isRepo: boolean;
31
- root: string | null;
32
- /** Current branch name, or `null` when HEAD is detached. */
33
- branch: string | null;
34
- detached: boolean;
35
- /** Short HEAD object name. */
36
- head: string | null;
37
- upstream: string | null;
38
- ahead: number;
39
- behind: number;
40
- staged: StatusFileEntry[];
41
- unstaged: StatusFileEntry[];
42
- untracked: string[];
43
- /** `true` when there are no staged, unstaged, or untracked changes. */
44
- clean: boolean;
45
- /** `true` when stage / unstage / commit actions are available. */
46
- canWrite: boolean;
47
- }
48
- //#endregion
49
- //#region src/rpc/functions/commit.d.ts
50
- interface CommitArgs {
51
- /** Commit message. */
52
- message: string;
53
- }
54
- interface CommitResult {
55
- /** `true` when the commit succeeded. */
56
- ok: boolean;
57
- /** Short hash of the new commit, or `null` on failure. */
58
- hash: string | null;
59
- /** Human-readable outcome (e.g. "nothing to commit"). */
60
- message: string;
61
- /** Working-tree status after the attempt. */
62
- status: GitStatus;
63
- }
64
- //#endregion
65
- //#region src/rpc/functions/diff.d.ts
66
- interface DiffFile {
67
- path: string;
68
- additions: number;
69
- deletions: number;
70
- binary: boolean;
71
- }
72
- interface GitDiff {
73
- isRepo: boolean;
74
- staged: boolean;
75
- path: string | null;
76
- files: DiffFile[];
77
- totalAdditions: number;
78
- totalDeletions: number;
79
- /** Unified patch text — populated when `path` targets a single file. */
80
- patch: string | null;
81
- /** `true` when `patch` was clipped to {@link PATCH_CHAR_LIMIT}. */
82
- truncated: boolean;
83
- }
84
- interface DiffArgs {
85
- /** Limit the diff to a single path; omit for the whole tree. */
86
- path?: string;
87
- /** Diff the index against HEAD instead of the working tree. */
88
- staged?: boolean;
89
- }
90
- //#endregion
91
- //#region src/rpc/functions/log.d.ts
92
- interface Commit {
93
- hash: string;
94
- shortHash: string;
95
- author: string;
96
- email: string;
97
- /** Author date as epoch milliseconds. */
98
- date: number;
99
- subject: string;
100
- body: string;
101
- /** Ref names pointing at this commit (branches, tags, HEAD). */
102
- refs: string[];
103
- /** Full parent hashes — drives the commit graph. */
104
- parents: string[];
105
- }
106
- interface GitLog {
107
- isRepo: boolean;
108
- commits: Commit[];
109
- limit: number;
110
- skip: number;
111
- /** `true` when the page filled to `limit`, hinting at further history. */
112
- hasMore: boolean;
113
- }
114
- interface LogArgs {
115
- /** Number of commits to return (clamped to 1–200, default 30). */
116
- limit?: number;
117
- /** Commits to skip from the tip, for pagination (default 0). */
118
- skip?: number;
119
- /** Optional ref/branch to read history from (default: current HEAD). */
120
- ref?: string;
121
- }
122
- //#endregion
123
- //#region src/rpc/functions/show.d.ts
124
- interface CommitFile {
125
- path: string;
126
- additions: number;
127
- deletions: number;
128
- binary: boolean;
129
- /** Change kind relative to the parent (add / modify / delete / rename …). */
130
- status: FileStatusCode;
131
- }
132
- interface CommitDetail {
133
- /** `false` when the working directory is not inside a git repository. */
134
- isRepo: boolean;
135
- /** `false` when the hash does not resolve to a commit. */
136
- found: boolean;
137
- hash: string;
138
- shortHash: string;
139
- author: string;
140
- email: string;
141
- /** Author date as epoch milliseconds. */
142
- date: number;
143
- committer: string;
144
- committerEmail: string;
145
- /** Commit date as epoch milliseconds. */
146
- commitDate: number;
147
- subject: string;
148
- body: string;
149
- parents: string[];
150
- refs: string[];
151
- files: CommitFile[];
152
- totalAdditions: number;
153
- totalDeletions: number;
154
- /** Unified patch text for the commit, or `null` when omitted/unavailable. */
155
- patch: string | null;
156
- /** `true` when `patch` was clipped to {@link PATCH_CHAR_LIMIT}. */
157
- truncated: boolean;
158
- }
159
- interface ShowArgs {
160
- /** Commit-ish to inspect (full or short hash). */
161
- hash: string;
162
- /** Include the full unified patch (default `true`). */
163
- patch?: boolean;
164
- }
165
- //#endregion
166
- //#region src/rpc/functions/stage.d.ts
167
- interface StageArgs {
168
- /** Paths to stage (`git add`). */
169
- paths: string[];
170
- }
171
- //#endregion
172
- //#region src/rpc/functions/unstage.d.ts
173
- interface UnstageArgs {
174
- /** Paths to unstage (`git restore --staged`). */
175
- paths: string[];
176
- }
177
- //#endregion
178
2
  //#region src/index.d.ts
179
3
  interface GitDevframeOptions {
180
4
  /** Repository directory to inspect. Defaults to the devframe `cwd`. */
@@ -188,11 +12,6 @@ interface GitDevframeOptions {
188
12
  distDir?: string;
189
13
  /** Preferred dev-server port (default 9710). */
190
14
  port?: number;
191
- /**
192
- * Enable staging, unstaging, and committing from the UI. Read-only by
193
- * default; the standalone CLI also accepts a `--write` flag.
194
- */
195
- write?: boolean;
196
15
  /**
197
16
  * Require the trust handshake on the standalone server. Enabled by
198
17
  * default — `--open` embeds the current OTP in the opened URL, so the
@@ -202,12 +21,15 @@ interface GitDevframeOptions {
202
21
  auth?: boolean;
203
22
  }
204
23
  /**
205
- * Create the Git dashboard devframe. Mount it into any host via devframe's
206
- * adapters, or run it standalone with the bundled CLI (`devframe-git`).
24
+ * Create the Git dashboard devframe. All git work runs through the
25
+ * `@devframes/service-git` wire service (declared below); the SPA calls its
26
+ * `devframes:service:git:*` RPC directly. Mount it into any host via
27
+ * devframe's adapters, or run it standalone with the bundled CLI
28
+ * (`devframe-git`).
207
29
  *
208
30
  * @experimental This plugin is experimental and may change without a major
209
31
  * version bump until it stabilizes.
210
32
  */
211
33
  declare function createGitDevframe(options?: GitDevframeOptions): DevframeDefinition;
212
34
  //#endregion
213
- export { type Branch, type Commit, type CommitArgs, type CommitDetail, type CommitFile, type CommitResult, type DiffArgs, type DiffFile, type FileStatusCode, type GitBranches, GitDevframeOptions, type GitDiff, type GitLog, type GitStatus, type LogArgs, type ShowArgs, type StageArgs, type StatusFileEntry, type UnstageArgs, createGitDevframe, createGitDevframe as default };
35
+ export { GitDevframeOptions, createGitDevframe, createGitDevframe as default };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as createGitDevframe } from "./src-CyRmAQrb.mjs";
1
+ import { t as createGitDevframe } from "./src-E7rY0DjA.mjs";
2
2
  export { createGitDevframe, createGitDevframe as default };
@@ -0,0 +1,74 @@
1
+ import process from "node:process";
2
+ import { defineDevframe } from "devframe";
3
+ import { resolve } from "pathe";
4
+ //#region package.json
5
+ var name = "@devframes/plugin-git";
6
+ var version = "0.9.3";
7
+ var description = "Devframe plugin that shows a Git dashboard for status, log, branches, and diffs.";
8
+ var homepage = "https://github.com/devframes/devframe#readme";
9
+ //#endregion
10
+ //#region src/index.ts
11
+ const remoteAssets = {
12
+ package: `${name}--assets`,
13
+ version
14
+ };
15
+ const GIT_SERVICE = "@devframes/service-git";
16
+ /**
17
+ * Create the Git dashboard devframe. All git work runs through the
18
+ * `@devframes/service-git` wire service (declared below); the SPA calls its
19
+ * `devframes:service:git:*` RPC directly. Mount it into any host via
20
+ * devframe's adapters, or run it standalone with the bundled CLI
21
+ * (`devframe-git`).
22
+ *
23
+ * @experimental This plugin is experimental and may change without a major
24
+ * version bump until it stabilizes.
25
+ */
26
+ function createGitDevframe(options = {}) {
27
+ const distDir = options.distDir ?? remoteAssets;
28
+ const cwd = options.repoRoot ? resolve(process.cwd(), options.repoRoot) : void 0;
29
+ return defineDevframe({
30
+ id: "devframes_plugin_git",
31
+ name: "Git",
32
+ version,
33
+ packageName: name,
34
+ importMetaUrl: import.meta.url,
35
+ homepage,
36
+ description,
37
+ icon: "ph:git-branch-duotone",
38
+ basePath: options.basePath,
39
+ cli: {
40
+ command: "devframe-git",
41
+ port: options.port ?? 9710,
42
+ distDir,
43
+ auth: options.auth ?? true
44
+ },
45
+ services: [{
46
+ package: GIT_SERVICE,
47
+ ...cwd ? { options: { cwd } } : {}
48
+ }, { package: "@devframes/service-shiki" }],
49
+ rpc: { snapshot: [
50
+ "devframes:service:git:status",
51
+ "devframes:service:git:branches",
52
+ "devframes:service:git:diff",
53
+ {
54
+ method: "devframes:service:git:log",
55
+ inputs: [[{ limit: 200 }]]
56
+ },
57
+ {
58
+ method: "devframes:service:git:show",
59
+ inputs: async (ctx) => {
60
+ const git = ctx.services.get(GIT_SERVICE);
61
+ if (!git) return [];
62
+ const { commits } = await git.log({ limit: 200 });
63
+ return commits.map((commit) => [{
64
+ hash: commit.hash,
65
+ patch: false
66
+ }]);
67
+ }
68
+ }
69
+ ] },
70
+ setup() {}
71
+ });
72
+ }
73
+ //#endregion
74
+ export { createGitDevframe as t };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devframes/plugin-git",
3
3
  "type": "module",
4
- "version": "0.9.1",
4
+ "version": "0.9.3",
5
5
  "description": "Devframe plugin that shows a Git dashboard for status, log, branches, and diffs.",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "dist"
34
34
  ],
35
35
  "peerDependencies": {
36
- "@devframes/plugin-git--assets": "0.9.1"
36
+ "@devframes/plugin-git--assets": "0.9.3"
37
37
  },
38
38
  "peerDependenciesMeta": {
39
39
  "@devframes/plugin-git--assets": {
@@ -43,35 +43,38 @@
43
43
  "dependencies": {
44
44
  "cac": "^7.0.0",
45
45
  "pathe": "^2.0.3",
46
- "devframe": "0.9.1"
46
+ "@devframes/service-git": "0.9.3",
47
+ "@devframes/service-shiki": "0.9.3",
48
+ "devframe": "0.9.3"
47
49
  },
48
50
  "devDependencies": {
49
51
  "@antfu/design": "^0.3.4",
50
52
  "@floating-ui/react": "^0.27.20",
51
53
  "@iconify-json/catppuccin": "^1.2.17",
52
- "@pierre/diffs": "^1.2.12",
53
54
  "@radix-ui/react-scroll-area": "^1.2.18",
54
55
  "@radix-ui/react-slot": "^1.3.3",
55
- "@storybook/addon-a11y": "^10.5.7",
56
- "@storybook/addon-docs": "^10.5.7",
57
- "@storybook/react-vite": "^10.5.7",
56
+ "@storybook/addon-a11y": "^10.5.8",
57
+ "@storybook/addon-docs": "^10.5.8",
58
+ "@storybook/react-vite": "^10.5.8",
58
59
  "@types/react": "^19.2.18",
59
60
  "@types/react-dom": "^19.2.4",
60
61
  "@unocss/postcss": "^66.7.5",
61
62
  "@vitejs/plugin-react-oxc": "^0.4.3",
62
63
  "clsx": "^2.1.1",
63
64
  "colorjs.io": "^0.7.1",
65
+ "diff": "^9.0.0",
64
66
  "h3": "^2.0.1-rc.26",
65
- "next": "^16.3.0",
67
+ "next": "^16.3.1",
66
68
  "react": "^19.2.8",
67
69
  "react-dom": "^19.2.8",
68
- "storybook": "^10.5.7",
70
+ "shiki": "^4.4.3",
71
+ "storybook": "^10.5.8",
69
72
  "tailwind-merge": "^3.6.0",
70
73
  "tsdown": "^0.22.14",
71
74
  "unocss": "^66.7.5",
72
75
  "vitest": "^4.1.10",
73
76
  "ws": "^8.21.3",
74
- "@devframes/plugin-git--assets": "0.9.1"
77
+ "@devframes/plugin-git--assets": "0.9.3"
75
78
  },
76
79
  "scripts": {
77
80
  "build": "tsdown && pnpm run build:spa",
@@ -1,933 +0,0 @@
1
- import { defineDevframe, defineRpcFunction } from "devframe";
2
- import { resolve } from "pathe";
3
- import { execFile } from "node:child_process";
4
- import process from "node:process";
5
- import { promisify } from "node:util";
6
- import { s } from "devframe/utils/simple-schema";
7
- //#region package.json
8
- var name = "@devframes/plugin-git";
9
- var version = "0.9.1";
10
- var description = "Devframe plugin that shows a Git dashboard for status, log, branches, and diffs.";
11
- var homepage = "https://github.com/devframes/devframe#readme";
12
- //#endregion
13
- //#region src/node/git.ts
14
- const execFileAsync = promisify(execFile);
15
- const MAX_BUFFER = 67108864;
16
- /**
17
- * Run a git command in `cwd`. Rejects when git exits non-zero — callers that
18
- * tolerate failure (e.g. "no upstream configured") should use {@link tryGit}.
19
- */
20
- async function runGit(cwd, args) {
21
- const { stdout, stderr } = await execFileAsync("git", args, {
22
- cwd,
23
- maxBuffer: MAX_BUFFER,
24
- windowsHide: true,
25
- env: {
26
- ...process.env,
27
- GIT_PAGER: "cat",
28
- GIT_OPTIONAL_LOCKS: "0",
29
- LC_ALL: "C"
30
- }
31
- });
32
- return {
33
- stdout,
34
- stderr
35
- };
36
- }
37
- /** Run a git command, returning trimmed stdout or `null` when it fails. */
38
- async function tryGit(cwd, args) {
39
- try {
40
- const { stdout } = await runGit(cwd, args);
41
- return stdout.replace(/\n$/, "");
42
- } catch {
43
- return null;
44
- }
45
- }
46
- /**
47
- * Client-supplied revisions must not start with `-`, which Git would parse as
48
- * an option before it treats the value as a revision.
49
- */
50
- function isSafeRevision(rev) {
51
- return rev.length > 0 && !rev.startsWith("-");
52
- }
53
- /** Resolve the repository root for `cwd`, or `null` when `cwd` is outside a repo. */
54
- async function resolveRepoRoot(cwd) {
55
- return tryGit(cwd, ["rev-parse", "--show-toplevel"]);
56
- }
57
- /** Split git output on a separator, dropping the trailing empty segment. */
58
- function splitClean(input, separator) {
59
- return input.split(separator).filter((part) => part.length > 0);
60
- }
61
- /**
62
- * Extract a concise, single-line message from a failed `execFile` error.
63
- * git writes useful text (e.g. "nothing to commit") to stdout/stderr.
64
- */
65
- function gitErrorMessage(error) {
66
- const e = error;
67
- return (e?.stderr || e?.stdout || e?.message || "git command failed").trim().split("\n")[0];
68
- }
69
- //#endregion
70
- //#region src/rpc/context.ts
71
- const configs = /* @__PURE__ */ new WeakMap();
72
- const contexts = /* @__PURE__ */ new WeakMap();
73
- /**
74
- * Record the working directory for a context. Called from the devframe
75
- * `setup` before any RPC handler runs, so {@link getGitContext} can honor a
76
- * `repoRoot` override instead of the raw `ctx.cwd`.
77
- */
78
- function configureGit(ctx, config) {
79
- configs.set(ctx, config);
80
- }
81
- /**
82
- * Per-`DevframeNodeContext` git state. Each RPC function file pulls its
83
- * working directory and (memoized) repo-root lookup from here instead of
84
- * re-resolving on every call.
85
- */
86
- function getGitContext(ctx) {
87
- let existing = contexts.get(ctx);
88
- if (existing) return existing;
89
- const config = configs.get(ctx);
90
- const cwd = config?.cwd ?? ctx.cwd;
91
- const write = config?.write ?? false;
92
- let rootPromise;
93
- existing = {
94
- cwd,
95
- write,
96
- resolveRoot: () => {
97
- rootPromise ??= resolveRepoRoot(cwd);
98
- return rootPromise;
99
- }
100
- };
101
- contexts.set(ctx, existing);
102
- return existing;
103
- }
104
- //#endregion
105
- //#region src/rpc/functions/branches.ts
106
- const FORMAT$1 = [
107
- "%(refname:short)",
108
- "%(objectname:short)",
109
- "%(HEAD)",
110
- "%(upstream:short)",
111
- "%(upstream:track)",
112
- "%(contents:subject)"
113
- ].join("");
114
- function parseTrack(track) {
115
- if (track.includes("gone")) return {
116
- ahead: 0,
117
- behind: 0,
118
- gone: true
119
- };
120
- const ahead = track.match(/ahead (\d+)/);
121
- const behind = track.match(/behind (\d+)/);
122
- return {
123
- ahead: ahead ? Number(ahead[1]) : 0,
124
- behind: behind ? Number(behind[1]) : 0,
125
- gone: false
126
- };
127
- }
128
- const branches = defineRpcFunction({
129
- name: "devframes:plugin:git:branches",
130
- type: "query",
131
- snapshot: true,
132
- jsonSerializable: true,
133
- agent: {
134
- description: "List local and remote branches of the inspected repository with tracking state (ahead/behind, gone upstreams) and the current branch. Safe to call freely.",
135
- title: "Git branches"
136
- },
137
- setup: (ctx) => {
138
- const git = getGitContext(ctx);
139
- return { handler: async () => {
140
- if (!await git.resolveRoot()) return {
141
- isRepo: false,
142
- current: null,
143
- branches: []
144
- };
145
- const raw = await tryGit(git.cwd, [
146
- "for-each-ref",
147
- `--format=${FORMAT$1}`,
148
- "refs/heads"
149
- ]);
150
- if (!raw) return {
151
- isRepo: true,
152
- current: null,
153
- branches: []
154
- };
155
- let current = null;
156
- const branches = splitClean(raw, "\n").map((line) => {
157
- const [name, sha, head, upstream, track, subject] = line.split("");
158
- const isCurrent = head === "*";
159
- if (isCurrent) current = name;
160
- return {
161
- name,
162
- current: isCurrent,
163
- sha,
164
- upstream: upstream || null,
165
- subject: subject ?? "",
166
- ...parseTrack(track ?? "")
167
- };
168
- });
169
- branches.sort((a, b) => Number(b.current) - Number(a.current));
170
- return {
171
- isRepo: true,
172
- current,
173
- branches
174
- };
175
- } };
176
- }
177
- });
178
- //#endregion
179
- //#region src/rpc/functions/status.ts
180
- const EMPTY_STATUS = {
181
- isRepo: false,
182
- root: null,
183
- branch: null,
184
- detached: false,
185
- head: null,
186
- upstream: null,
187
- ahead: 0,
188
- behind: 0,
189
- staged: [],
190
- unstaged: [],
191
- untracked: [],
192
- clean: true,
193
- canWrite: false
194
- };
195
- function mapCode(code) {
196
- switch (code) {
197
- case "M": return "modified";
198
- case "A": return "added";
199
- case "D": return "deleted";
200
- case "R": return "renamed";
201
- case "C": return "copied";
202
- case "T": return "type-changed";
203
- case "U": return "unmerged";
204
- default: return "unknown";
205
- }
206
- }
207
- /**
208
- * Parse `git status --porcelain=v2 --branch -z` into a structured snapshot.
209
- * Records are NUL-separated; rename/copy (type `2`) entries consume an extra
210
- * token for the original path.
211
- */
212
- function parseStatus(root, raw) {
213
- const tokens = raw.split("\0");
214
- const status = {
215
- ...EMPTY_STATUS,
216
- isRepo: true,
217
- root,
218
- staged: [],
219
- unstaged: [],
220
- untracked: []
221
- };
222
- for (let i = 0; i < tokens.length; i++) {
223
- const token = tokens[i];
224
- if (!token) continue;
225
- if (token.startsWith("# ")) {
226
- const [, key, ...rest] = token.split(" ");
227
- const value = rest.join(" ");
228
- if (key === "branch.head") {
229
- if (value === "(detached)") {
230
- status.detached = true;
231
- status.branch = null;
232
- } else status.branch = value;
233
- } else if (key === "branch.oid" && value !== "(initial)") status.head = value.slice(0, 9);
234
- else if (key === "branch.upstream") status.upstream = value;
235
- else if (key === "branch.ab") {
236
- const match = value.match(/\+(\d+)\s+-(\d+)/);
237
- if (match) {
238
- status.ahead = Number(match[1]);
239
- status.behind = Number(match[2]);
240
- }
241
- }
242
- continue;
243
- }
244
- if (token.startsWith("1 ") || token.startsWith("2 ")) {
245
- const renamed = token.startsWith("2 ");
246
- const fields = token.split(" ");
247
- const xy = fields[1];
248
- const x = xy[0];
249
- const y = xy[1];
250
- const path = fields.slice(renamed ? 9 : 8).join(" ");
251
- const from = renamed ? tokens[++i] : void 0;
252
- if (x !== ".") status.staged.push(from ? {
253
- path,
254
- from,
255
- status: mapCode(x)
256
- } : {
257
- path,
258
- status: mapCode(x)
259
- });
260
- if (y !== ".") status.unstaged.push({
261
- path,
262
- status: mapCode(y)
263
- });
264
- continue;
265
- }
266
- if (token.startsWith("u ")) {
267
- const path = token.split(" ").slice(10).join(" ");
268
- status.unstaged.push({
269
- path,
270
- status: "unmerged"
271
- });
272
- continue;
273
- }
274
- if (token.startsWith("? ")) status.untracked.push(token.slice(2));
275
- }
276
- status.clean = status.staged.length === 0 && status.unstaged.length === 0 && status.untracked.length === 0;
277
- return status;
278
- }
279
- /**
280
- * Read the working-tree status for a git context. Shared by the `devframes:plugin:git:status`
281
- * query and the write actions (which return fresh status after mutating).
282
- */
283
- async function readStatus(git) {
284
- const root = await git.resolveRoot();
285
- if (!root) return {
286
- ...EMPTY_STATUS,
287
- canWrite: false
288
- };
289
- const { stdout } = await runGit(git.cwd, [
290
- "status",
291
- "--porcelain=v2",
292
- "--branch",
293
- "-z"
294
- ]);
295
- const status = parseStatus(root, stdout);
296
- status.canWrite = git.write;
297
- return status;
298
- }
299
- const status = defineRpcFunction({
300
- name: "devframes:plugin:git:status",
301
- type: "query",
302
- snapshot: true,
303
- jsonSerializable: true,
304
- agent: {
305
- description: "Working-tree status of the inspected repository: current branch, ahead/behind counts, and every staged/unstaged/untracked file. Call this first to orient before reading diffs or history. Safe to call freely.",
306
- title: "Git status"
307
- },
308
- setup: (ctx) => {
309
- const git = getGitContext(ctx);
310
- return { handler: () => readStatus(git) };
311
- }
312
- });
313
- //#endregion
314
- //#region src/rpc/functions/commit.ts
315
- const commit = defineRpcFunction({
316
- name: "devframes:plugin:git:commit",
317
- type: "action",
318
- jsonSerializable: true,
319
- setup: (ctx) => {
320
- const git = getGitContext(ctx);
321
- return { handler: async (args) => {
322
- const message = (args?.message ?? "").trim();
323
- if (!await git.resolveRoot()) return {
324
- ok: false,
325
- hash: null,
326
- message: "Not a git repository.",
327
- status: await readStatus(git)
328
- };
329
- if (!message) return {
330
- ok: false,
331
- hash: null,
332
- message: "Commit message is required.",
333
- status: await readStatus(git)
334
- };
335
- try {
336
- await runGit(git.cwd, [
337
- "commit",
338
- "-m",
339
- message
340
- ]);
341
- return {
342
- ok: true,
343
- hash: await tryGit(git.cwd, [
344
- "rev-parse",
345
- "--short",
346
- "HEAD"
347
- ]),
348
- message: "Committed.",
349
- status: await readStatus(git)
350
- };
351
- } catch (error) {
352
- return {
353
- ok: false,
354
- hash: null,
355
- message: gitErrorMessage(error),
356
- status: await readStatus(git)
357
- };
358
- }
359
- } };
360
- }
361
- });
362
- //#endregion
363
- //#region src/rpc/functions/diff.ts
364
- /** Hard cap on the returned patch text to keep payloads bounded. */
365
- const PATCH_CHAR_LIMIT$1 = 2e5;
366
- const diffFileSchema = s.object({
367
- path: s.string(),
368
- additions: s.number(),
369
- deletions: s.number(),
370
- binary: s.boolean()
371
- });
372
- const gitDiffSchema = s.object({
373
- isRepo: s.boolean(),
374
- staged: s.boolean(),
375
- path: s.nullable(s.string()),
376
- files: s.array(diffFileSchema),
377
- totalAdditions: s.number(),
378
- totalDeletions: s.number(),
379
- patch: s.nullable(s.string()),
380
- truncated: s.boolean()
381
- });
382
- function parseNumstat$1(raw) {
383
- return splitClean(raw, "\n").map((line) => {
384
- const [add, del, ...rest] = line.split(" ");
385
- const binary = add === "-" || del === "-";
386
- return {
387
- path: rest.join(" "),
388
- additions: binary ? 0 : Number(add),
389
- deletions: binary ? 0 : Number(del),
390
- binary
391
- };
392
- });
393
- }
394
- const diff = defineRpcFunction({
395
- name: "devframes:plugin:git:diff",
396
- type: "query",
397
- snapshot: true,
398
- jsonSerializable: true,
399
- args: [s.object({
400
- path: s.optional(s.string()),
401
- staged: s.optional(s.boolean())
402
- })],
403
- returns: gitDiffSchema,
404
- agent: {
405
- description: "Unified diff of uncommitted changes in the inspected repository — the working tree by default, the index with staged: true, one file with path. Call before summarizing or reviewing in-progress work. Safe to call freely.",
406
- title: "Git diff"
407
- },
408
- setup: (ctx) => {
409
- const git = getGitContext(ctx);
410
- return { handler: async (args = {}) => {
411
- const { path, staged = false } = args;
412
- if (!await git.resolveRoot()) return {
413
- isRepo: false,
414
- staged,
415
- path: path ?? null,
416
- files: [],
417
- totalAdditions: 0,
418
- totalDeletions: 0,
419
- patch: null,
420
- truncated: false
421
- };
422
- const base = staged ? ["diff", "--cached"] : ["diff"];
423
- const scope = path ? ["--", path] : [];
424
- const numstatRaw = await tryGit(git.cwd, [
425
- ...base,
426
- "--numstat",
427
- ...scope
428
- ]);
429
- const files = numstatRaw ? parseNumstat$1(numstatRaw) : [];
430
- const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0);
431
- const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0);
432
- let patch = null;
433
- let truncated = false;
434
- if (path) {
435
- const { stdout } = await runGit(git.cwd, [...base, ...scope]);
436
- if (stdout.length > PATCH_CHAR_LIMIT$1) {
437
- patch = stdout.slice(0, PATCH_CHAR_LIMIT$1);
438
- truncated = true;
439
- } else patch = stdout;
440
- }
441
- return {
442
- isRepo: true,
443
- staged,
444
- path: path ?? null,
445
- files,
446
- totalAdditions,
447
- totalDeletions,
448
- patch,
449
- truncated
450
- };
451
- } };
452
- }
453
- });
454
- //#endregion
455
- //#region src/rpc/functions/log.ts
456
- const commitSchema = s.object({
457
- hash: s.string(),
458
- shortHash: s.string(),
459
- author: s.string(),
460
- email: s.string(),
461
- date: s.number(),
462
- subject: s.string(),
463
- body: s.string(),
464
- refs: s.array(s.string()),
465
- parents: s.array(s.string())
466
- });
467
- const gitLogSchema = s.object({
468
- isRepo: s.boolean(),
469
- commits: s.array(commitSchema),
470
- limit: s.number(),
471
- skip: s.number(),
472
- hasMore: s.boolean()
473
- });
474
- const FORMAT = [
475
- "%H",
476
- "%h",
477
- "%P",
478
- "%an",
479
- "%ae",
480
- "%aI",
481
- "%D",
482
- "%s",
483
- "%b"
484
- ].join("") + "";
485
- function clamp(value, min, max) {
486
- return Math.min(Math.max(value, min), max);
487
- }
488
- function parseLog(raw) {
489
- return splitClean(raw, "").map((record) => {
490
- const [hash, shortHash, parents, author, email, isoDate, refs, subject, body] = record.replace(/^\n/, "").split("");
491
- return {
492
- hash,
493
- shortHash,
494
- author,
495
- email,
496
- date: Date.parse(isoDate),
497
- subject,
498
- body: (body ?? "").trim(),
499
- refs: refs ? refs.split(", ").map((r) => r.trim()).filter(Boolean) : [],
500
- parents: parents ? parents.split(" ").filter(Boolean) : []
501
- };
502
- });
503
- }
504
- const SNAPSHOT_LIMIT$1 = 200;
505
- const log = defineRpcFunction({
506
- name: "devframes:plugin:git:log",
507
- type: "query",
508
- jsonSerializable: true,
509
- args: [s.object({
510
- limit: s.optional(s.number()),
511
- skip: s.optional(s.number()),
512
- ref: s.optional(s.string())
513
- })],
514
- returns: gitLogSchema,
515
- agent: {
516
- description: "Commit history of the inspected repository, newest first. Paginate with limit (1-200, default 30) and skip; pass ref to read another branch. Call before reasoning about recent changes. Safe to call freely.",
517
- title: "Git log"
518
- },
519
- dump: async (_ctx, handler) => {
520
- const baked = {
521
- ...await handler({
522
- limit: SNAPSHOT_LIMIT$1,
523
- skip: 0
524
- }),
525
- hasMore: false
526
- };
527
- return {
528
- records: [{
529
- inputs: [],
530
- output: baked
531
- }],
532
- fallback: baked
533
- };
534
- },
535
- setup: (ctx) => {
536
- const git = getGitContext(ctx);
537
- return { handler: async (args = {}) => {
538
- const limit = clamp(Math.trunc(args.limit ?? 30), 1, 200);
539
- const skip = Math.max(0, Math.trunc(args.skip ?? 0));
540
- const ref = args.ref?.trim() || void 0;
541
- if (!await git.resolveRoot()) return {
542
- isRepo: false,
543
- commits: [],
544
- limit,
545
- skip,
546
- hasMore: false
547
- };
548
- const command = [
549
- "log",
550
- "--topo-order",
551
- `--max-count=${limit}`,
552
- `--skip=${skip}`,
553
- `--pretty=format:${FORMAT}`
554
- ];
555
- if (ref) {
556
- if (!isSafeRevision(ref)) return {
557
- isRepo: true,
558
- commits: [],
559
- limit,
560
- skip,
561
- hasMore: false
562
- };
563
- command.push("--end-of-options", ref);
564
- }
565
- const raw = await tryGit(git.cwd, command);
566
- const commits = raw ? parseLog(raw) : [];
567
- return {
568
- isRepo: true,
569
- commits,
570
- limit,
571
- skip,
572
- hasMore: commits.length === limit
573
- };
574
- } };
575
- }
576
- });
577
- //#endregion
578
- //#region src/rpc/functions/show.ts
579
- /** Hard cap on the returned patch text to keep payloads bounded. */
580
- const PATCH_CHAR_LIMIT = 2e5;
581
- /** Matches the window `devframes:plugin:git:log` bakes, so any visible commit has a snapshot. */
582
- const SNAPSHOT_LIMIT = 200;
583
- /** Read-only git detail work can run in parallel without overwhelming builds. */
584
- const DUMP_CONCURRENCY = 8;
585
- const fileStatusCodeSchema = s.picklist([
586
- "modified",
587
- "added",
588
- "deleted",
589
- "renamed",
590
- "copied",
591
- "type-changed",
592
- "unmerged",
593
- "unknown"
594
- ]);
595
- const commitFileSchema = s.object({
596
- path: s.string(),
597
- additions: s.number(),
598
- deletions: s.number(),
599
- binary: s.boolean(),
600
- status: fileStatusCodeSchema
601
- });
602
- const commitDetailSchema = s.object({
603
- isRepo: s.boolean(),
604
- found: s.boolean(),
605
- hash: s.string(),
606
- shortHash: s.string(),
607
- author: s.string(),
608
- email: s.string(),
609
- date: s.number(),
610
- committer: s.string(),
611
- committerEmail: s.string(),
612
- commitDate: s.number(),
613
- subject: s.string(),
614
- body: s.string(),
615
- parents: s.array(s.string()),
616
- refs: s.array(s.string()),
617
- files: s.array(commitFileSchema),
618
- totalAdditions: s.number(),
619
- totalDeletions: s.number(),
620
- patch: s.nullable(s.string()),
621
- truncated: s.boolean()
622
- });
623
- const EMPTY_DETAIL = {
624
- isRepo: false,
625
- found: false,
626
- hash: "",
627
- shortHash: "",
628
- author: "",
629
- email: "",
630
- date: 0,
631
- committer: "",
632
- committerEmail: "",
633
- commitDate: 0,
634
- subject: "",
635
- body: "",
636
- parents: [],
637
- refs: [],
638
- files: [],
639
- totalAdditions: 0,
640
- totalDeletions: 0,
641
- patch: null,
642
- truncated: false
643
- };
644
- const SHOW_FORMAT = [
645
- "%H",
646
- "%h",
647
- "%P",
648
- "%an",
649
- "%ae",
650
- "%aI",
651
- "%cn",
652
- "%ce",
653
- "%cI",
654
- "%D",
655
- "%s",
656
- "%b"
657
- ].join("");
658
- function mapStatusCode(code) {
659
- switch (code[0]) {
660
- case "M": return "modified";
661
- case "A": return "added";
662
- case "D": return "deleted";
663
- case "R": return "renamed";
664
- case "C": return "copied";
665
- case "T": return "type-changed";
666
- case "U": return "unmerged";
667
- default: return "unknown";
668
- }
669
- }
670
- /**
671
- * Parse `git diff-tree --name-status` into a `path → status` map. Rename/copy
672
- * rows carry a similarity score and both old + new paths; the new path (last
673
- * field) keys the map so it aligns with the numstat entry.
674
- */
675
- function parseNameStatus(raw) {
676
- const map = /* @__PURE__ */ new Map();
677
- for (const line of splitClean(raw, "\n")) {
678
- const [code, ...paths] = line.split(" ");
679
- const path = paths[paths.length - 1];
680
- if (path) map.set(path, mapStatusCode(code));
681
- }
682
- return map;
683
- }
684
- function parseNumstat(raw, status) {
685
- return splitClean(raw, "\n").map((line) => {
686
- const [add, del, ...rest] = line.split(" ");
687
- const binary = add === "-" || del === "-";
688
- const path = rest.join(" ");
689
- return {
690
- path,
691
- additions: binary ? 0 : Number(add),
692
- deletions: binary ? 0 : Number(del),
693
- binary,
694
- status: status.get(path) ?? "modified"
695
- };
696
- });
697
- }
698
- async function readCommit(git, hash, includePatch) {
699
- if (!isSafeRevision(hash)) return {
700
- ...EMPTY_DETAIL,
701
- isRepo: true
702
- };
703
- const meta = await tryGit(git.cwd, [
704
- "show",
705
- "-s",
706
- `--format=${SHOW_FORMAT}`,
707
- "--end-of-options",
708
- hash
709
- ]);
710
- if (meta == null) return {
711
- ...EMPTY_DETAIL,
712
- isRepo: true
713
- };
714
- const [fullHash, shortHash, parents, author, email, authorDate, committer, committerEmail, committerDate, refs, subject, body] = meta.split("");
715
- const numstat = await tryGit(git.cwd, [
716
- "diff-tree",
717
- "--no-commit-id",
718
- "--numstat",
719
- "-r",
720
- "--root",
721
- "--end-of-options",
722
- hash
723
- ]);
724
- const nameStatusRaw = await tryGit(git.cwd, [
725
- "diff-tree",
726
- "--no-commit-id",
727
- "--name-status",
728
- "-r",
729
- "--root",
730
- "--end-of-options",
731
- hash
732
- ]);
733
- const files = numstat ? parseNumstat(numstat, nameStatusRaw ? parseNameStatus(nameStatusRaw) : /* @__PURE__ */ new Map()) : [];
734
- const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0);
735
- const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0);
736
- let patch = null;
737
- let truncated = false;
738
- if (includePatch) {
739
- const raw = await tryGit(git.cwd, [
740
- "diff-tree",
741
- "-p",
742
- "--no-commit-id",
743
- "-r",
744
- "--root",
745
- "--end-of-options",
746
- hash
747
- ]);
748
- if (raw != null) {
749
- if (raw.length > PATCH_CHAR_LIMIT) {
750
- patch = raw.slice(0, PATCH_CHAR_LIMIT);
751
- truncated = true;
752
- } else patch = raw;
753
- }
754
- }
755
- return {
756
- isRepo: true,
757
- found: true,
758
- hash: fullHash,
759
- shortHash,
760
- author,
761
- email,
762
- date: Date.parse(authorDate),
763
- committer,
764
- committerEmail,
765
- commitDate: Date.parse(committerDate),
766
- subject,
767
- body: (body ?? "").trim(),
768
- parents: parents ? parents.split(" ").filter(Boolean) : [],
769
- refs: refs ? refs.split(", ").map((r) => r.trim()).filter(Boolean) : [],
770
- files,
771
- totalAdditions,
772
- totalDeletions,
773
- patch,
774
- truncated
775
- };
776
- }
777
- const show = defineRpcFunction({
778
- name: "devframes:plugin:git:show",
779
- type: "query",
780
- jsonSerializable: true,
781
- args: [s.object({
782
- hash: s.string(),
783
- patch: s.optional(s.boolean())
784
- })],
785
- returns: commitDetailSchema,
786
- agent: {
787
- description: "Full detail of one commit by hash (from the git log tool): metadata, changed files, and the unified patch (pass patch: false to skip it for large commits). Safe to call freely.",
788
- title: "Git show"
789
- },
790
- dump: async (ctx, _handler) => {
791
- const git = getGitContext(ctx);
792
- if (!await git.resolveRoot()) return {
793
- records: [],
794
- fallback: EMPTY_DETAIL
795
- };
796
- const raw = await tryGit(git.cwd, [
797
- "log",
798
- "--topo-order",
799
- `--max-count=${SNAPSHOT_LIMIT}`,
800
- "--pretty=format:%H"
801
- ]);
802
- const hashes = raw ? raw.split("\n").filter(Boolean) : [];
803
- const records = [];
804
- for (let i = 0; i < hashes.length; i += DUMP_CONCURRENCY) {
805
- const batch = hashes.slice(i, i + DUMP_CONCURRENCY);
806
- const outputs = await Promise.all(batch.map((hash) => readCommit(git, hash, false)));
807
- batch.forEach((hash, index) => {
808
- records.push({
809
- inputs: [{ hash }],
810
- output: outputs[index]
811
- });
812
- });
813
- }
814
- return {
815
- records,
816
- fallback: records[0]?.output ?? {
817
- ...EMPTY_DETAIL,
818
- isRepo: true
819
- }
820
- };
821
- },
822
- setup: (ctx) => {
823
- const git = getGitContext(ctx);
824
- return { handler: async (args) => {
825
- const hash = (args?.hash ?? "").trim();
826
- const includePatch = args?.patch ?? true;
827
- if (!await git.resolveRoot() || !hash) return EMPTY_DETAIL;
828
- return readCommit(git, hash, includePatch);
829
- } };
830
- }
831
- });
832
- //#endregion
833
- //#region src/rpc/functions/stage.ts
834
- const stage = defineRpcFunction({
835
- name: "devframes:plugin:git:stage",
836
- type: "action",
837
- jsonSerializable: true,
838
- setup: (ctx) => {
839
- const git = getGitContext(ctx);
840
- return { handler: async (args) => {
841
- const paths = args?.paths ?? [];
842
- if (await git.resolveRoot() && paths.length > 0) await runGit(git.cwd, [
843
- "add",
844
- "--",
845
- ...paths
846
- ]);
847
- return readStatus(git);
848
- } };
849
- }
850
- });
851
- //#endregion
852
- //#region src/rpc/functions/unstage.ts
853
- const unstage = defineRpcFunction({
854
- name: "devframes:plugin:git:unstage",
855
- type: "action",
856
- jsonSerializable: true,
857
- setup: (ctx) => {
858
- const git = getGitContext(ctx);
859
- return { handler: async (args) => {
860
- const paths = args?.paths ?? [];
861
- if (await git.resolveRoot() && paths.length > 0) await runGit(git.cwd, [
862
- "restore",
863
- "--staged",
864
- "--",
865
- ...paths
866
- ]);
867
- return readStatus(git);
868
- } };
869
- }
870
- });
871
- //#endregion
872
- //#region src/rpc/index.ts
873
- /** Read-only RPC — always registered. */
874
- const readFunctions = [
875
- status,
876
- log,
877
- show,
878
- branches,
879
- diff
880
- ];
881
- /** Mutating RPC — registered only when write actions are enabled. */
882
- const writeFunctions = [
883
- stage,
884
- unstage,
885
- commit
886
- ];
887
- //#endregion
888
- //#region src/index.ts
889
- const remoteAssets = {
890
- package: `${name}--assets`,
891
- version,
892
- resolveFrom: import.meta.url
893
- };
894
- /**
895
- * Create the Git dashboard devframe. Mount it into any host via devframe's
896
- * adapters, or run it standalone with the bundled CLI (`devframe-git`).
897
- *
898
- * @experimental This plugin is experimental and may change without a major
899
- * version bump until it stabilizes.
900
- */
901
- function createGitDevframe(options = {}) {
902
- const distDir = options.distDir ?? remoteAssets;
903
- return defineDevframe({
904
- id: "devframes_plugin_git",
905
- name: "Git",
906
- version,
907
- packageName: name,
908
- homepage,
909
- description,
910
- icon: "ph:git-branch-duotone",
911
- basePath: options.basePath,
912
- cli: {
913
- command: "devframe-git",
914
- port: options.port ?? 9710,
915
- distDir,
916
- auth: options.auth ?? true,
917
- configure(cli) {
918
- cli.option("--write", "Enable staging, unstaging, and committing from the UI");
919
- }
920
- },
921
- setup(ctx, info) {
922
- const write = options.write ?? info?.flags?.write === true;
923
- configureGit(ctx, {
924
- cwd: options.repoRoot ? resolve(options.repoRoot) : ctx.cwd,
925
- write
926
- });
927
- for (const fn of readFunctions) ctx.rpc.register(fn);
928
- if (write) for (const fn of writeFunctions) ctx.rpc.register(fn);
929
- }
930
- });
931
- }
932
- //#endregion
933
- export { createGitDevframe as t };