@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.
- package/ARCHITECTURE.md +66 -0
- package/LICENSE +674 -0
- package/README.md +66 -0
- package/bin/git.js +446 -0
- package/bin/openvcs-git-plugin.js +10 -0
- package/bin/plugin-helpers.js +149 -0
- package/bin/plugin-request-handler.js +336 -0
- package/bin/plugin-runtime.js +38 -0
- package/bin/plugin-types.js +3 -0
- package/bin/plugin.js +53 -0
- package/bin/submodules.js +190 -0
- package/icon.png +0 -0
- package/package.json +48 -0
- package/src/git.ts +615 -0
- package/src/plugin-helpers.ts +184 -0
- package/src/plugin-request-handler.ts +593 -0
- package/src/plugin-runtime.ts +52 -0
- package/src/plugin-types.ts +35 -0
- package/src/plugin.ts +75 -0
- package/src/submodules.ts +219 -0
- package/tsconfig.json +16 -0
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Copyright © 2025-2026 OpenVCS Contributors
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
import type { PluginModuleDefinition } from '@openvcs/sdk/runtime';
|
|
5
|
+
import { getOrCreateMenu, registerAction, invoke } from '@openvcs/sdk/runtime';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
GitVcsDelegates,
|
|
9
|
+
type GitRuntimeDependencies,
|
|
10
|
+
} from './plugin-request-handler.js';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
allocateSession,
|
|
14
|
+
closeSession,
|
|
15
|
+
requireSession,
|
|
16
|
+
} from './plugin-runtime.js';
|
|
17
|
+
|
|
18
|
+
import { registerSubmoduleToolkit } from './submodules.js';
|
|
19
|
+
|
|
20
|
+
import { GitCommand } from './git.js';
|
|
21
|
+
|
|
22
|
+
/** Creates a GitCommand instance for a given repository path. */
|
|
23
|
+
function createGitCommand(cwd: string): GitCommand {
|
|
24
|
+
return new GitCommand(cwd);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Returns the runtime services passed to the Git VCS delegate class. */
|
|
28
|
+
function createGitRuntimeDependencies(): GitRuntimeDependencies {
|
|
29
|
+
return {
|
|
30
|
+
allocateSession,
|
|
31
|
+
closeSession,
|
|
32
|
+
requireSession,
|
|
33
|
+
createGitCommand,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Registers the Git plugin with the OpenVCS SDK runtime.
|
|
38
|
+
*
|
|
39
|
+
* Provides Git runtime options up front and defers `vcs.*` delegate registration
|
|
40
|
+
* until `OnPluginStart()` validates the local Git installation. */
|
|
41
|
+
export const PluginDefinition: PluginModuleDefinition = {
|
|
42
|
+
logTarget: 'openvcs.git.plugin',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Validates Git installation and version at plugin startup.
|
|
46
|
+
*
|
|
47
|
+
* Runs before the runtime begins processing requests. Throws if Git is not
|
|
48
|
+
* installed or version is below 2.20.
|
|
49
|
+
* @throws Error if Git is not available or version is unsupported */
|
|
50
|
+
export function OnPluginStart(): void {
|
|
51
|
+
const git = new GitCommand(process.cwd());
|
|
52
|
+
const { major, minor } = git.version();
|
|
53
|
+
|
|
54
|
+
if (major < 2 || (major === 2 && minor < 20)) {
|
|
55
|
+
throw new Error(`Git 2.20+ required, found ${major}.${minor}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const delegates = new GitVcsDelegates(createGitRuntimeDependencies());
|
|
59
|
+
PluginDefinition.vcs = delegates.toDelegates();
|
|
60
|
+
|
|
61
|
+
const repoMenu = getOrCreateMenu('repository', 'Repository');
|
|
62
|
+
if (repoMenu) {
|
|
63
|
+
repoMenu.addItem({ label: 'Edit .gitignore', action: 'repo-edit-gitignore' });
|
|
64
|
+
repoMenu.addItem({ label: 'Edit .gitattributes', action: 'repo-edit-gitattributes' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
registerSubmoduleToolkit();
|
|
68
|
+
|
|
69
|
+
registerAction('repo-edit-gitignore', async () => {
|
|
70
|
+
await invoke('open_repo_dotfile', { name: '.gitignore' });
|
|
71
|
+
});
|
|
72
|
+
registerAction('repo-edit-gitattributes', async () => {
|
|
73
|
+
await invoke('open_repo_dotfile', { name: '.gitattributes' });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// Copyright © 2025-2026 OpenVCS Contributors
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
import { getOrCreateMenu, registerAction, ModalBuilder } from '@openvcs/sdk/runtime';
|
|
5
|
+
|
|
6
|
+
import { GitCommand, type SubmoduleEntry } from './git.js';
|
|
7
|
+
|
|
8
|
+
type ModalActionPayload = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
/** Returns a Git command bound to the current process working directory. */
|
|
11
|
+
function createGitCommand(): GitCommand {
|
|
12
|
+
return new GitCommand(process.cwd());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Coerces an unknown action payload into a plain record. */
|
|
16
|
+
function asPayload(value: unknown): ModalActionPayload {
|
|
17
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
return value as ModalActionPayload;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Returns one trimmed string field from an action payload. */
|
|
24
|
+
function payloadString(payload: ModalActionPayload, key: string): string {
|
|
25
|
+
return String(payload[key] ?? '').trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Builds one modal row for a submodule entry. */
|
|
29
|
+
function buildSubmoduleRow(entry: SubmoduleEntry) {
|
|
30
|
+
const metaBits = [entry.commit ? `commit ${entry.commit}` : '', entry.branch ? `branch ${entry.branch}` : '']
|
|
31
|
+
.map((part) => String(part || '').trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
|
|
34
|
+
const description = [entry.url, metaBits.join(' · ')]
|
|
35
|
+
.map((part) => String(part || '').trim())
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.join(' · ');
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
id: entry.path,
|
|
41
|
+
title: entry.path,
|
|
42
|
+
status: entry.state,
|
|
43
|
+
meta: entry.name,
|
|
44
|
+
description,
|
|
45
|
+
actions: [
|
|
46
|
+
{ type: 'button' as const, id: 'submodules-update', content: 'Update', payload: { path: entry.path } },
|
|
47
|
+
{ type: 'button' as const, id: 'submodules-sync', content: 'Sync', payload: { path: entry.path } },
|
|
48
|
+
{
|
|
49
|
+
type: 'button' as const,
|
|
50
|
+
id: 'submodules-remove-request',
|
|
51
|
+
content: 'Remove',
|
|
52
|
+
variant: 'danger' as const,
|
|
53
|
+
payload: { path: entry.path, name: entry.name },
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Builds and opens the submodule manager modal. */
|
|
60
|
+
async function openSubmodulesModal(): Promise<unknown> {
|
|
61
|
+
const git = createGitCommand();
|
|
62
|
+
const entries = git.listSubmodules();
|
|
63
|
+
console.log('Git submodules: building modal', { count: entries.length });
|
|
64
|
+
|
|
65
|
+
const modal = new ModalBuilder('Manage Submodules')
|
|
66
|
+
.text('Review, add, update, sync, and remove submodules without leaving Git.')
|
|
67
|
+
.separator()
|
|
68
|
+
.input('url', 'Submodule URL', {
|
|
69
|
+
kind: 'url',
|
|
70
|
+
placeholder: 'https://example.com/repo.git',
|
|
71
|
+
})
|
|
72
|
+
.input('path', 'Submodule Path', {
|
|
73
|
+
placeholder: 'libs/example',
|
|
74
|
+
})
|
|
75
|
+
.input('name', 'Submodule Name', {
|
|
76
|
+
placeholder: 'example',
|
|
77
|
+
})
|
|
78
|
+
.input('branch', 'Branch (optional)', {
|
|
79
|
+
placeholder: 'main',
|
|
80
|
+
})
|
|
81
|
+
.button('submodules-add', 'Add Submodule', {
|
|
82
|
+
variant: 'primary',
|
|
83
|
+
align: 'centered',
|
|
84
|
+
})
|
|
85
|
+
.button('repo-submodules', 'Refresh', {
|
|
86
|
+
align: 'centered',
|
|
87
|
+
})
|
|
88
|
+
.button('submodules-update-all', 'Update All (Recursive)', {
|
|
89
|
+
align: 'centered',
|
|
90
|
+
})
|
|
91
|
+
.button('submodules-sync-all', 'Sync All', {
|
|
92
|
+
align: 'centered',
|
|
93
|
+
})
|
|
94
|
+
.separator()
|
|
95
|
+
.list('submodules', {
|
|
96
|
+
label: 'Submodules',
|
|
97
|
+
emptyText: 'This repository has no submodules yet.',
|
|
98
|
+
items: entries.map((entry) => buildSubmoduleRow(entry)),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return modal.open();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Builds and opens the remove confirmation modal for one submodule. */
|
|
105
|
+
async function openRemoveConfirmationModal(path: string, name?: string): Promise<unknown> {
|
|
106
|
+
const submodulePath = String(path || '').trim();
|
|
107
|
+
if (!submodulePath) return;
|
|
108
|
+
console.log('Git submodules: building remove confirmation', { path: submodulePath, name });
|
|
109
|
+
|
|
110
|
+
const modal = new ModalBuilder('Confirm Submodule Removal')
|
|
111
|
+
.text(`Remove the submodule at ${submodulePath}? This will deinitialize the submodule, remove it from the index, and delete its working tree entry.`)
|
|
112
|
+
.button('repo-submodules', 'Back', {
|
|
113
|
+
align: 'centered',
|
|
114
|
+
})
|
|
115
|
+
.button('submodules-remove-confirm', 'Remove Submodule', {
|
|
116
|
+
variant: 'danger',
|
|
117
|
+
align: 'centered',
|
|
118
|
+
payload: { path: submodulePath, name: String(name || '').trim() || undefined },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return modal.open();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Removes one submodule and refreshes the toolkit modal. */
|
|
125
|
+
async function removeSubmodule(payload: ModalActionPayload): Promise<void> {
|
|
126
|
+
const path = payloadString(payload, 'path');
|
|
127
|
+
if (!path) return;
|
|
128
|
+
const git = createGitCommand();
|
|
129
|
+
git.removeSubmodule(path);
|
|
130
|
+
await openSubmodulesModal();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Adds one submodule and refreshes the toolkit modal. */
|
|
134
|
+
async function addSubmodule(payload: ModalActionPayload): Promise<void> {
|
|
135
|
+
const url = payloadString(payload, 'url');
|
|
136
|
+
const path = payloadString(payload, 'path');
|
|
137
|
+
const name = payloadString(payload, 'name');
|
|
138
|
+
const branch = payloadString(payload, 'branch');
|
|
139
|
+
|
|
140
|
+
if (!url || !path) {
|
|
141
|
+
throw new Error('Submodule URL and path are required');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const git = createGitCommand();
|
|
145
|
+
git.addSubmodule(url, path, name || undefined, branch || undefined);
|
|
146
|
+
await openSubmodulesModal();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Updates one submodule and refreshes the toolkit modal. */
|
|
150
|
+
async function updateSubmodule(payload: ModalActionPayload): Promise<void> {
|
|
151
|
+
const path = payloadString(payload, 'path');
|
|
152
|
+
if (!path) return;
|
|
153
|
+
const git = createGitCommand();
|
|
154
|
+
git.updateSubmodule(path);
|
|
155
|
+
await openSubmodulesModal();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Syncs one submodule and refreshes the toolkit modal. */
|
|
159
|
+
async function syncSubmodule(payload: ModalActionPayload): Promise<void> {
|
|
160
|
+
const path = payloadString(payload, 'path');
|
|
161
|
+
if (!path) return;
|
|
162
|
+
const git = createGitCommand();
|
|
163
|
+
git.syncSubmodule(path);
|
|
164
|
+
await openSubmodulesModal();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Updates all submodules recursively and refreshes the toolkit modal. */
|
|
168
|
+
async function updateAllSubmodules(): Promise<void> {
|
|
169
|
+
const git = createGitCommand();
|
|
170
|
+
git.updateAllSubmodules();
|
|
171
|
+
await openSubmodulesModal();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Syncs all submodules recursively and refreshes the toolkit modal. */
|
|
175
|
+
async function syncAllSubmodules(): Promise<void> {
|
|
176
|
+
const git = createGitCommand();
|
|
177
|
+
git.syncAllSubmodules();
|
|
178
|
+
await openSubmodulesModal();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Registers the Git submodule toolkit menu and action handlers. */
|
|
182
|
+
export function registerSubmoduleToolkit(): void {
|
|
183
|
+
const repoMenu = getOrCreateMenu('repository', 'Repository');
|
|
184
|
+
repoMenu?.addItem({ label: 'Submodules', action: 'repo-submodules' });
|
|
185
|
+
|
|
186
|
+
registerAction('repo-submodules', async () => {
|
|
187
|
+
console.log('Git submodules: repo-submodules action invoked');
|
|
188
|
+
return openSubmodulesModal();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
registerAction('submodules-add', async (payload?: unknown) => {
|
|
192
|
+
return addSubmodule(asPayload(payload));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
registerAction('submodules-update-all', async () => {
|
|
196
|
+
return updateAllSubmodules();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
registerAction('submodules-sync-all', async () => {
|
|
200
|
+
return syncAllSubmodules();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
registerAction('submodules-update', async (payload?: unknown) => {
|
|
204
|
+
return updateSubmodule(asPayload(payload));
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
registerAction('submodules-sync', async (payload?: unknown) => {
|
|
208
|
+
return syncSubmodule(asPayload(payload));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
registerAction('submodules-remove-request', async (payload?: unknown) => {
|
|
212
|
+
const data = asPayload(payload);
|
|
213
|
+
return openRemoveConfirmationModal(payloadString(data, 'path'), payloadString(data, 'name'));
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
registerAction('submodules-remove-confirm', async (payload?: unknown) => {
|
|
217
|
+
return removeSubmodule(asPayload(payload));
|
|
218
|
+
});
|
|
219
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"outDir": "bin",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"types": ["node"],
|
|
10
|
+
"lib": ["ES2022"],
|
|
11
|
+
"noEmitOnError": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"verbatimModuleSyntax": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"]
|
|
16
|
+
}
|