@openvcs/git-plugin 0.1.0

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.
@@ -0,0 +1,336 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+ import { VcsDelegateBase, pluginError, } from '@openvcs/sdk/runtime';
4
+ import { asNumber, asRecord, asString, asStringArray, asTrimmedString, } from './plugin-helpers.js';
5
+ import { GitCommand } from './git.js';
6
+ /** Implements the Git-backed `vcs.*` delegate surface for the SDK runtime. */
7
+ export class GitVcsDelegates extends VcsDelegateBase {
8
+ /** Returns the required repository worktree path for a session id. */
9
+ requireSessionPath(sessionId) {
10
+ return this.deps.requireSession(sessionId).path;
11
+ }
12
+ /** Returns a Git command helper bound to a required session. */
13
+ requireGit(sessionId) {
14
+ const cwd = this.requireSessionPath(sessionId);
15
+ return this.deps.createGitCommand(cwd);
16
+ }
17
+ getCaps(_params, _context) {
18
+ return {
19
+ commits: true,
20
+ branches: true,
21
+ tags: true,
22
+ staging: true,
23
+ push_pull: true,
24
+ fast_forward: true,
25
+ };
26
+ }
27
+ open(params, _context) {
28
+ const repoPath = asTrimmedString(params.path);
29
+ if (!repoPath) {
30
+ throw pluginError('vcs-open-invalid-path', 'path is required');
31
+ }
32
+ const git = this.deps.createGitCommand(repoPath);
33
+ git.runChecked(['rev-parse', '--git-dir'], 'vcs-open-not-repository');
34
+ const sessionId = this.deps.allocateSession({ path: repoPath });
35
+ return { session_id: sessionId };
36
+ }
37
+ close(params, _context) {
38
+ this.deps.closeSession(params.session_id);
39
+ return null;
40
+ }
41
+ cloneRepo(params, context) {
42
+ const url = asTrimmedString(params.url);
43
+ const destination = asTrimmedString(params.dest);
44
+ if (!url || !destination) {
45
+ throw pluginError('vcs-clone-invalid-args', 'url and dest are required');
46
+ }
47
+ const git = this.deps.createGitCommand(process.cwd());
48
+ const output = git.runChecked(['clone', url, destination], 'vcs-clone-failed');
49
+ const lines = `${output.stdout}\n${output.stderr}`
50
+ .split(/\r?\n/g)
51
+ .map((line) => line.trim())
52
+ .filter(Boolean);
53
+ for (const line of lines) {
54
+ context.host.info(line);
55
+ }
56
+ return null;
57
+ }
58
+ getWorkdir(params, _context) {
59
+ return this.requireSessionPath(params.session_id);
60
+ }
61
+ getCurrentBranch(params, _context) {
62
+ const git = this.requireGit(params.session_id);
63
+ const branch = git.currentBranch();
64
+ return branch === 'HEAD' ? null : branch;
65
+ }
66
+ listBranches(params, _context) {
67
+ const git = this.requireGit(params.session_id);
68
+ const raw = git.runChecked([
69
+ 'for-each-ref',
70
+ '--format=%(refname:short)\t%(refname)\t%(HEAD)',
71
+ 'refs/heads',
72
+ 'refs/remotes',
73
+ ], 'vcs-list-branches-failed');
74
+ return raw.stdout
75
+ .split(/\r?\n/g)
76
+ .map((line) => line.trim())
77
+ .filter(Boolean)
78
+ .map((line) => {
79
+ const [name = '', fullRef = '', headMark = ''] = line.split('\t');
80
+ const isRemote = fullRef.startsWith('refs/remotes/');
81
+ const remote = isRemote ? name.split('/')[0] ?? null : null;
82
+ const kind = isRemote
83
+ ? { type: 'Remote', remote }
84
+ : { type: 'Local' };
85
+ return {
86
+ name,
87
+ full_ref: fullRef,
88
+ kind,
89
+ current: headMark === '*',
90
+ };
91
+ });
92
+ }
93
+ listLocalBranches(params, _context) {
94
+ const git = this.requireGit(params.session_id);
95
+ const raw = git.runChecked(['for-each-ref', '--format=%(refname:short)', 'refs/heads/'], 'vcs-list-branches-failed');
96
+ return raw.stdout
97
+ .split('\n')
98
+ .map((branch) => branch.trim())
99
+ .filter(Boolean);
100
+ }
101
+ createBranch(params, _context) {
102
+ const git = this.requireGit(params.session_id);
103
+ git.createBranch(asTrimmedString(params.name));
104
+ return null;
105
+ }
106
+ checkoutBranch(params, _context) {
107
+ const git = this.requireGit(params.session_id);
108
+ git.checkoutBranch(asTrimmedString(params.name));
109
+ return null;
110
+ }
111
+ ensureRemote(params, _context) {
112
+ const git = this.requireGit(params.session_id);
113
+ const name = asTrimmedString(params.name);
114
+ const url = asTrimmedString(params.url);
115
+ if (!name || !url) {
116
+ throw pluginError('vcs-remote-invalid-args', 'name and url are required');
117
+ }
118
+ git.ensureRemote(name, url);
119
+ return null;
120
+ }
121
+ listRemotes(params, _context) {
122
+ const git = this.requireGit(params.session_id);
123
+ const result = git.listRemotes();
124
+ return result.remotes.map((remote) => ({
125
+ name: remote.name,
126
+ url: remote.fetch || remote.push,
127
+ }));
128
+ }
129
+ removeRemote(params, _context) {
130
+ const git = this.requireGit(params.session_id);
131
+ git.removeRemote(asTrimmedString(params.name));
132
+ return null;
133
+ }
134
+ fetch(params, _context) {
135
+ const git = this.requireGit(params.session_id);
136
+ const options = asRecord(params.opts);
137
+ git.fetch({
138
+ remote: asTrimmedString(params.remote) || undefined,
139
+ refspec: asTrimmedString(params.refspec) || undefined,
140
+ opts: { prune: options.prune === true },
141
+ });
142
+ return null;
143
+ }
144
+ push(params, _context) {
145
+ const git = this.requireGit(params.session_id);
146
+ git.push({
147
+ remote: asTrimmedString(params.remote) || undefined,
148
+ refspec: asTrimmedString(params.refspec) || undefined,
149
+ });
150
+ return null;
151
+ }
152
+ pullFfOnly(params, _context) {
153
+ const git = this.requireGit(params.session_id);
154
+ git.pull({
155
+ remote: asTrimmedString(params.remote) || undefined,
156
+ branch: asTrimmedString(params.branch) || undefined,
157
+ });
158
+ return null;
159
+ }
160
+ commit(params, _context) {
161
+ const git = this.requireGit(params.session_id);
162
+ const result = git.runChecked(['rev-parse', 'HEAD'], 'git-commit-failed');
163
+ git.commit(asTrimmedString(params.message));
164
+ return result.stdout.trim();
165
+ }
166
+ commitIndex(params, _context) {
167
+ const git = this.requireGit(params.session_id);
168
+ const result = git.runChecked(['rev-parse', 'HEAD'], 'git-commit-failed');
169
+ git.commitIndex(asTrimmedString(params.message), asTrimmedString(params.name), asTrimmedString(params.email), asStringArray(params.paths));
170
+ return result.stdout.trim();
171
+ }
172
+ getStatusSummary(params, _context) {
173
+ const git = this.requireGit(params.session_id);
174
+ return git.status().summary;
175
+ }
176
+ getStatusPayload(params, _context) {
177
+ const git = this.requireGit(params.session_id);
178
+ return git.status().payload;
179
+ }
180
+ listCommits(params, _context) {
181
+ const git = this.requireGit(params.session_id);
182
+ const query = asRecord(params.query);
183
+ const result = git.listCommits({
184
+ branch: asTrimmedString(query.rev) || undefined,
185
+ skip: asNumber(query.skip, 0) || undefined,
186
+ limit: asNumber(query.limit, 0),
187
+ topo_order: query.topo_order ?? undefined,
188
+ include_merges: query.include_merges ?? undefined,
189
+ author_contains: asTrimmedString(query.author_contains) || undefined,
190
+ since_utc: asTrimmedString(query.since_utc) || undefined,
191
+ until_utc: asTrimmedString(query.until_utc) || undefined,
192
+ path: asTrimmedString(query.path) || undefined,
193
+ });
194
+ return result.commits;
195
+ }
196
+ diffFile(params, _context) {
197
+ const git = this.requireGit(params.session_id);
198
+ return git.diffFile(asTrimmedString(params.path)).split('\n');
199
+ }
200
+ diffCommit(params, _context) {
201
+ const git = this.requireGit(params.session_id);
202
+ return git.diffCommit(asTrimmedString(params.rev)).split('\n');
203
+ }
204
+ getConflictDetails(params, _context) {
205
+ const git = this.requireGit(params.session_id);
206
+ return git.getConflictDetails(asTrimmedString(params.path));
207
+ }
208
+ checkoutConflictSide(params, _context) {
209
+ const git = this.requireGit(params.session_id);
210
+ const side = asTrimmedString(params.side);
211
+ if (side !== 'ours' && side !== 'theirs') {
212
+ throw pluginError('vcs-invalid-side', 'side must be "ours" or "theirs"');
213
+ }
214
+ git.checkoutConflictSide(asTrimmedString(params.path), side);
215
+ return null;
216
+ }
217
+ writeMergeResult(params, _context) {
218
+ const git = this.requireGit(params.session_id);
219
+ const content = Buffer.from(asTrimmedString(params.content_b64), 'base64').toString('utf8');
220
+ git.writeMergeResult(asTrimmedString(params.path), content);
221
+ return null;
222
+ }
223
+ stagePatch(params, _context) {
224
+ const git = this.requireGit(params.session_id);
225
+ git.stagePatch(asString(params.patch));
226
+ return null;
227
+ }
228
+ discardPaths(params, _context) {
229
+ const git = this.requireGit(params.session_id);
230
+ const paths = asStringArray(params.paths);
231
+ if (paths.length === 0) {
232
+ return null;
233
+ }
234
+ git.runChecked(['checkout', '--', ...paths], 'git-discard-paths-failed');
235
+ return null;
236
+ }
237
+ applyReversePatch(params, _context) {
238
+ const git = this.requireGit(params.session_id);
239
+ git.applyReversePatch(asString(params.patch));
240
+ return null;
241
+ }
242
+ deleteBranch(params, _context) {
243
+ const git = this.requireGit(params.session_id);
244
+ git.deleteBranch(asTrimmedString(params.name));
245
+ return null;
246
+ }
247
+ renameBranch(params, _context) {
248
+ const git = this.requireGit(params.session_id);
249
+ git.renameBranch(asTrimmedString(params.old), asTrimmedString(params.new));
250
+ return null;
251
+ }
252
+ mergeIntoCurrent(params, _context) {
253
+ const git = this.requireGit(params.session_id);
254
+ git.mergeIntoCurrent(asTrimmedString(params.name));
255
+ return null;
256
+ }
257
+ mergeAbort(params, _context) {
258
+ const git = this.requireGit(params.session_id);
259
+ git.mergeAbort();
260
+ return null;
261
+ }
262
+ mergeContinue(params, _context) {
263
+ const git = this.requireGit(params.session_id);
264
+ git.mergeContinue(asTrimmedString(params.message) || undefined);
265
+ return null;
266
+ }
267
+ isMergeInProgress(params, _context) {
268
+ const git = this.requireGit(params.session_id);
269
+ return git.isMergeInProgress();
270
+ }
271
+ setBranchUpstream(params, _context) {
272
+ const git = this.requireGit(params.session_id);
273
+ git.setBranchUpstream(asTrimmedString(params.branch), asTrimmedString(params.upstream));
274
+ return null;
275
+ }
276
+ getBranchUpstream(params, _context) {
277
+ const git = this.requireGit(params.session_id);
278
+ return git.getBranchUpstream(asTrimmedString(params.branch));
279
+ }
280
+ hardResetHead(params, _context) {
281
+ const git = this.requireGit(params.session_id);
282
+ git.hardResetHead(params.ref);
283
+ return null;
284
+ }
285
+ resetSoftTo(params, _context) {
286
+ const git = this.requireGit(params.session_id);
287
+ git.resetSoftTo(asTrimmedString(params.rev));
288
+ return null;
289
+ }
290
+ getIdentity(params, _context) {
291
+ const git = this.requireGit(params.session_id);
292
+ return git.getIdentity();
293
+ }
294
+ setIdentityLocal(params, _context) {
295
+ const git = this.requireGit(params.session_id);
296
+ git.setIdentityLocal(asTrimmedString(params.name), asTrimmedString(params.email));
297
+ return null;
298
+ }
299
+ listStashes(params, _context) {
300
+ const git = this.requireGit(params.session_id);
301
+ return git.listStashes();
302
+ }
303
+ stashPush(params, _context) {
304
+ const git = this.requireGit(params.session_id);
305
+ return git.stashPush(asTrimmedString(params.message) || undefined, params.include_untracked);
306
+ }
307
+ stashApply(params, _context) {
308
+ const git = this.requireGit(params.session_id);
309
+ git.stashApply(asString(params.selector));
310
+ return null;
311
+ }
312
+ stashPop(params, _context) {
313
+ const git = this.requireGit(params.session_id);
314
+ git.stashPop(asString(params.selector));
315
+ return null;
316
+ }
317
+ stashDrop(params, _context) {
318
+ const git = this.requireGit(params.session_id);
319
+ git.stashDrop(asString(params.selector));
320
+ return null;
321
+ }
322
+ stashShow(params, _context) {
323
+ const git = this.requireGit(params.session_id);
324
+ return git.stashShow(asString(params.selector));
325
+ }
326
+ cherryPick(params, _context) {
327
+ const git = this.requireGit(params.session_id);
328
+ git.cherryPick(asTrimmedString(params.commit));
329
+ return null;
330
+ }
331
+ revertCommit(params, _context) {
332
+ const git = this.requireGit(params.session_id);
333
+ git.revertCommit(asTrimmedString(params.commit), params.no_edit);
334
+ return null;
335
+ }
336
+ }
@@ -0,0 +1,38 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+ import { pluginError } from '@openvcs/sdk/runtime';
4
+ import { asString } from './plugin-helpers.js';
5
+ /** Stores the next session id allocated for `vcs.open`. Allocated once per plugin runtime instance
6
+ * and persists for the lifetime of the process. Session IDs are opaque integers assigned
7
+ * sequentially starting from 1. */
8
+ let nextSessionId = 1;
9
+ /** Stores all active repository sessions keyed by session id. */
10
+ const sessions = new Map();
11
+ /** Allocates a new repository session and returns its generated id. */
12
+ export function allocateSession(session) {
13
+ const sessionId = String(nextSessionId);
14
+ nextSessionId += 1;
15
+ sessions.set(sessionId, session);
16
+ return sessionId;
17
+ }
18
+ /** Removes an existing repository session. */
19
+ export function closeSession(sessionId) {
20
+ const key = asString(sessionId);
21
+ if (!sessions.has(key)) {
22
+ throw pluginError('vcs-invalid-session', `session '${key}' not found`);
23
+ }
24
+ sessions.delete(key);
25
+ }
26
+ /** Resets all session state. Useful for testing to ensure clean state between test runs. */
27
+ export function resetSessions() {
28
+ nextSessionId = 1;
29
+ sessions.clear();
30
+ }
31
+ /** Resolves a required session or throws a host-facing plugin error. */
32
+ export function requireSession(sessionId) {
33
+ const session = sessions.get(asString(sessionId));
34
+ if (!session) {
35
+ throw pluginError('vcs-invalid-session', `unknown session '${asString(sessionId)}'`);
36
+ }
37
+ return session;
38
+ }
@@ -0,0 +1,3 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+ export {};
package/bin/plugin.js ADDED
@@ -0,0 +1,53 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+ import { getOrCreateMenu, registerAction, invoke } from '@openvcs/sdk/runtime';
4
+ import { GitVcsDelegates, } from './plugin-request-handler.js';
5
+ import { allocateSession, closeSession, requireSession, } from './plugin-runtime.js';
6
+ import { registerSubmoduleToolkit } from './submodules.js';
7
+ import { GitCommand } from './git.js';
8
+ /** Creates a GitCommand instance for a given repository path. */
9
+ function createGitCommand(cwd) {
10
+ return new GitCommand(cwd);
11
+ }
12
+ /** Returns the runtime services passed to the Git VCS delegate class. */
13
+ function createGitRuntimeDependencies() {
14
+ return {
15
+ allocateSession,
16
+ closeSession,
17
+ requireSession,
18
+ createGitCommand,
19
+ };
20
+ }
21
+ /** Registers the Git plugin with the OpenVCS SDK runtime.
22
+ *
23
+ * Provides Git runtime options up front and defers `vcs.*` delegate registration
24
+ * until `OnPluginStart()` validates the local Git installation. */
25
+ export const PluginDefinition = {
26
+ logTarget: 'openvcs.git.plugin',
27
+ };
28
+ /** Validates Git installation and version at plugin startup.
29
+ *
30
+ * Runs before the runtime begins processing requests. Throws if Git is not
31
+ * installed or version is below 2.20.
32
+ * @throws Error if Git is not available or version is unsupported */
33
+ export function OnPluginStart() {
34
+ const git = new GitCommand(process.cwd());
35
+ const { major, minor } = git.version();
36
+ if (major < 2 || (major === 2 && minor < 20)) {
37
+ throw new Error(`Git 2.20+ required, found ${major}.${minor}`);
38
+ }
39
+ const delegates = new GitVcsDelegates(createGitRuntimeDependencies());
40
+ PluginDefinition.vcs = delegates.toDelegates();
41
+ const repoMenu = getOrCreateMenu('repository', 'Repository');
42
+ if (repoMenu) {
43
+ repoMenu.addItem({ label: 'Edit .gitignore', action: 'repo-edit-gitignore' });
44
+ repoMenu.addItem({ label: 'Edit .gitattributes', action: 'repo-edit-gitattributes' });
45
+ }
46
+ registerSubmoduleToolkit();
47
+ registerAction('repo-edit-gitignore', async () => {
48
+ await invoke('open_repo_dotfile', { name: '.gitignore' });
49
+ });
50
+ registerAction('repo-edit-gitattributes', async () => {
51
+ await invoke('open_repo_dotfile', { name: '.gitattributes' });
52
+ });
53
+ }
@@ -0,0 +1,190 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+ import { getOrCreateMenu, registerAction, ModalBuilder } from '@openvcs/sdk/runtime';
4
+ import { GitCommand } from './git.js';
5
+ /** Returns a Git command bound to the current process working directory. */
6
+ function createGitCommand() {
7
+ return new GitCommand(process.cwd());
8
+ }
9
+ /** Coerces an unknown action payload into a plain record. */
10
+ function asPayload(value) {
11
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
12
+ return {};
13
+ }
14
+ return value;
15
+ }
16
+ /** Returns one trimmed string field from an action payload. */
17
+ function payloadString(payload, key) {
18
+ return String(payload[key] ?? '').trim();
19
+ }
20
+ /** Builds one modal row for a submodule entry. */
21
+ function buildSubmoduleRow(entry) {
22
+ const metaBits = [entry.commit ? `commit ${entry.commit}` : '', entry.branch ? `branch ${entry.branch}` : '']
23
+ .map((part) => String(part || '').trim())
24
+ .filter(Boolean);
25
+ const description = [entry.url, metaBits.join(' · ')]
26
+ .map((part) => String(part || '').trim())
27
+ .filter(Boolean)
28
+ .join(' · ');
29
+ return {
30
+ id: entry.path,
31
+ title: entry.path,
32
+ status: entry.state,
33
+ meta: entry.name,
34
+ description,
35
+ actions: [
36
+ { type: 'button', id: 'submodules-update', content: 'Update', payload: { path: entry.path } },
37
+ { type: 'button', id: 'submodules-sync', content: 'Sync', payload: { path: entry.path } },
38
+ {
39
+ type: 'button',
40
+ id: 'submodules-remove-request',
41
+ content: 'Remove',
42
+ variant: 'danger',
43
+ payload: { path: entry.path, name: entry.name },
44
+ },
45
+ ],
46
+ };
47
+ }
48
+ /** Builds and opens the submodule manager modal. */
49
+ async function openSubmodulesModal() {
50
+ const git = createGitCommand();
51
+ const entries = git.listSubmodules();
52
+ console.log('Git submodules: building modal', { count: entries.length });
53
+ const modal = new ModalBuilder('Manage Submodules')
54
+ .text('Review, add, update, sync, and remove submodules without leaving Git.')
55
+ .separator()
56
+ .input('url', 'Submodule URL', {
57
+ kind: 'url',
58
+ placeholder: 'https://example.com/repo.git',
59
+ })
60
+ .input('path', 'Submodule Path', {
61
+ placeholder: 'libs/example',
62
+ })
63
+ .input('name', 'Submodule Name', {
64
+ placeholder: 'example',
65
+ })
66
+ .input('branch', 'Branch (optional)', {
67
+ placeholder: 'main',
68
+ })
69
+ .button('submodules-add', 'Add Submodule', {
70
+ variant: 'primary',
71
+ align: 'centered',
72
+ })
73
+ .button('repo-submodules', 'Refresh', {
74
+ align: 'centered',
75
+ })
76
+ .button('submodules-update-all', 'Update All (Recursive)', {
77
+ align: 'centered',
78
+ })
79
+ .button('submodules-sync-all', 'Sync All', {
80
+ align: 'centered',
81
+ })
82
+ .separator()
83
+ .list('submodules', {
84
+ label: 'Submodules',
85
+ emptyText: 'This repository has no submodules yet.',
86
+ items: entries.map((entry) => buildSubmoduleRow(entry)),
87
+ });
88
+ return modal.open();
89
+ }
90
+ /** Builds and opens the remove confirmation modal for one submodule. */
91
+ async function openRemoveConfirmationModal(path, name) {
92
+ const submodulePath = String(path || '').trim();
93
+ if (!submodulePath)
94
+ return;
95
+ console.log('Git submodules: building remove confirmation', { path: submodulePath, name });
96
+ const modal = new ModalBuilder('Confirm Submodule Removal')
97
+ .text(`Remove the submodule at ${submodulePath}? This will deinitialize the submodule, remove it from the index, and delete its working tree entry.`)
98
+ .button('repo-submodules', 'Back', {
99
+ align: 'centered',
100
+ })
101
+ .button('submodules-remove-confirm', 'Remove Submodule', {
102
+ variant: 'danger',
103
+ align: 'centered',
104
+ payload: { path: submodulePath, name: String(name || '').trim() || undefined },
105
+ });
106
+ return modal.open();
107
+ }
108
+ /** Removes one submodule and refreshes the toolkit modal. */
109
+ async function removeSubmodule(payload) {
110
+ const path = payloadString(payload, 'path');
111
+ if (!path)
112
+ return;
113
+ const git = createGitCommand();
114
+ git.removeSubmodule(path);
115
+ await openSubmodulesModal();
116
+ }
117
+ /** Adds one submodule and refreshes the toolkit modal. */
118
+ async function addSubmodule(payload) {
119
+ const url = payloadString(payload, 'url');
120
+ const path = payloadString(payload, 'path');
121
+ const name = payloadString(payload, 'name');
122
+ const branch = payloadString(payload, 'branch');
123
+ if (!url || !path) {
124
+ throw new Error('Submodule URL and path are required');
125
+ }
126
+ const git = createGitCommand();
127
+ git.addSubmodule(url, path, name || undefined, branch || undefined);
128
+ await openSubmodulesModal();
129
+ }
130
+ /** Updates one submodule and refreshes the toolkit modal. */
131
+ async function updateSubmodule(payload) {
132
+ const path = payloadString(payload, 'path');
133
+ if (!path)
134
+ return;
135
+ const git = createGitCommand();
136
+ git.updateSubmodule(path);
137
+ await openSubmodulesModal();
138
+ }
139
+ /** Syncs one submodule and refreshes the toolkit modal. */
140
+ async function syncSubmodule(payload) {
141
+ const path = payloadString(payload, 'path');
142
+ if (!path)
143
+ return;
144
+ const git = createGitCommand();
145
+ git.syncSubmodule(path);
146
+ await openSubmodulesModal();
147
+ }
148
+ /** Updates all submodules recursively and refreshes the toolkit modal. */
149
+ async function updateAllSubmodules() {
150
+ const git = createGitCommand();
151
+ git.updateAllSubmodules();
152
+ await openSubmodulesModal();
153
+ }
154
+ /** Syncs all submodules recursively and refreshes the toolkit modal. */
155
+ async function syncAllSubmodules() {
156
+ const git = createGitCommand();
157
+ git.syncAllSubmodules();
158
+ await openSubmodulesModal();
159
+ }
160
+ /** Registers the Git submodule toolkit menu and action handlers. */
161
+ export function registerSubmoduleToolkit() {
162
+ const repoMenu = getOrCreateMenu('repository', 'Repository');
163
+ repoMenu?.addItem({ label: 'Submodules', action: 'repo-submodules' });
164
+ registerAction('repo-submodules', async () => {
165
+ console.log('Git submodules: repo-submodules action invoked');
166
+ return openSubmodulesModal();
167
+ });
168
+ registerAction('submodules-add', async (payload) => {
169
+ return addSubmodule(asPayload(payload));
170
+ });
171
+ registerAction('submodules-update-all', async () => {
172
+ return updateAllSubmodules();
173
+ });
174
+ registerAction('submodules-sync-all', async () => {
175
+ return syncAllSubmodules();
176
+ });
177
+ registerAction('submodules-update', async (payload) => {
178
+ return updateSubmodule(asPayload(payload));
179
+ });
180
+ registerAction('submodules-sync', async (payload) => {
181
+ return syncSubmodule(asPayload(payload));
182
+ });
183
+ registerAction('submodules-remove-request', async (payload) => {
184
+ const data = asPayload(payload);
185
+ return openRemoveConfirmationModal(payloadString(data, 'path'), payloadString(data, 'name'));
186
+ });
187
+ registerAction('submodules-remove-confirm', async (payload) => {
188
+ return removeSubmodule(asPayload(payload));
189
+ });
190
+ }
package/icon.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@openvcs/git-plugin",
3
+ "version": "0.1.0",
4
+ "description": "OpenVCS Git plugin - Node.js runtime",
5
+ "license": "GPL-3.0-or-later",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
10
+ "openvcs": {
11
+ "id": "openvcs.git",
12
+ "name": "Git",
13
+ "version": "0.1.0",
14
+ "author": "OpenVCS Contributors",
15
+ "description": "Git VCS backend plugin for OpenVCS",
16
+ "default_enabled": true,
17
+ "module": {
18
+ "exec": "openvcs-git-plugin.js",
19
+ "vcs_backends": [
20
+ "git"
21
+ ]
22
+ }
23
+ },
24
+ "scripts": {
25
+ "lint": "tsc -p tsconfig.json --noEmit",
26
+ "test": "tsx --test test/*.test.ts",
27
+ "prepack": "npm run build",
28
+ "build:plugin": "tsc -p tsconfig.json",
29
+ "build": "node ./node_modules/@openvcs/sdk/bin/openvcs.js build"
30
+ },
31
+ "dependencies": {
32
+ "@openvcs/sdk": "^0.2.16"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^25.5.0",
36
+ "tsx": "^4.20.6",
37
+ "typescript": "^6.0.2"
38
+ },
39
+ "files": [
40
+ "bin/",
41
+ "src/",
42
+ "tsconfig.json",
43
+ "README.md",
44
+ "ARCHITECTURE.md",
45
+ "icon.png",
46
+ "LICENSE"
47
+ ]
48
+ }