@openvcs/sdk 0.4.1 → 0.4.2-edge.20260613.129
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 +9 -6
- package/lib/build.d.ts +4 -4
- package/lib/build.js +36 -9
- package/lib/runtime/vcs-delegate-base.d.ts +3 -1
- package/lib/runtime/vcs-delegate-base.js +4 -0
- package/lib/runtime/vcs-delegate-metadata.d.ts +2 -0
- package/lib/runtime/vcs-delegate-metadata.js +1 -0
- package/lib/types/vcs.d.ts +34 -10
- package/package.json +1 -1
- package/src/lib/build.ts +47 -8
- package/src/lib/runtime/vcs-delegate-base.ts +9 -1
- package/src/lib/runtime/vcs-delegate-metadata.ts +2 -0
- package/src/lib/types/vcs.ts +40 -11
- package/test/build.test.js +54 -2
- package/test/vcs-delegate-base.test.js +66 -0
package/README.md
CHANGED
|
@@ -174,6 +174,9 @@ compiled plugin author module at `bin/plugin.js`, and then generates the SDK-own
|
|
|
174
174
|
`bin/<module.exec>` bootstrap that imports `./plugin.js`, applies `PluginDefinition`,
|
|
175
175
|
invokes `OnPluginStart()`, and starts the runtime.
|
|
176
176
|
|
|
177
|
+
The build prints progress messages to stderr so you can see each phase. Pass `-V`
|
|
178
|
+
or `--verbose` to also echo the exact command and validation details.
|
|
179
|
+
|
|
177
180
|
Theme-only plugins can also run `npm run build`; the command exits successfully
|
|
178
181
|
without producing `bin/` output.
|
|
179
182
|
|
|
@@ -208,18 +211,18 @@ Dependency behavior:
|
|
|
208
211
|
|
|
209
212
|
## CLI usage
|
|
210
213
|
|
|
211
|
-
Build a plugin
|
|
214
|
+
Build plugin assets from a plugin project:
|
|
212
215
|
|
|
213
216
|
```bash
|
|
214
|
-
|
|
217
|
+
npm run build
|
|
215
218
|
```
|
|
216
219
|
|
|
217
|
-
Show command help:
|
|
220
|
+
Show command help through npm script:
|
|
218
221
|
|
|
219
222
|
```bash
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
+
npm run openvcs -- --help
|
|
224
|
+
npm run openvcs -- build --help
|
|
225
|
+
npm run openvcs -- init --help
|
|
223
226
|
```
|
|
224
227
|
|
|
225
228
|
## Publishing Note
|
package/lib/build.d.ts
CHANGED
|
@@ -16,17 +16,17 @@ export declare function buildUsage(commandName?: string): string;
|
|
|
16
16
|
/** Parses `openvcs build` arguments. */
|
|
17
17
|
export declare function parseBuildArgs(args: string[]): BuildArgs;
|
|
18
18
|
/** Reads and validates the plugin manifest. */
|
|
19
|
-
export declare function readManifest(pluginDir: string): ManifestInfo;
|
|
19
|
+
export declare function readManifest(pluginDir: string, verbose?: boolean): ManifestInfo;
|
|
20
20
|
/** Verifies that a declared module entry resolves to a real file under `bin/`. */
|
|
21
|
-
export declare function validateDeclaredModuleExec(pluginDir: string, moduleExec: string | undefined): void;
|
|
21
|
+
export declare function validateDeclaredModuleExec(pluginDir: string, moduleExec: string | undefined, verbose?: boolean): void;
|
|
22
22
|
/** Returns the compiled plugin module path imported by the generated bootstrap. */
|
|
23
23
|
export declare function authoredPluginModulePath(pluginDir: string): string;
|
|
24
24
|
/** Ensures the plugin's authored module and generated bootstrap paths are compatible. */
|
|
25
|
-
export declare function validateGeneratedBootstrapTargets(pluginDir: string, moduleExec: string | undefined): void;
|
|
25
|
+
export declare function validateGeneratedBootstrapTargets(pluginDir: string, moduleExec: string | undefined, verbose?: boolean): void;
|
|
26
26
|
/** Renders the generated Node bootstrap that owns runtime startup. */
|
|
27
27
|
export declare function renderGeneratedBootstrap(pluginModuleImportPath: string, isEsm: boolean): string;
|
|
28
28
|
/** Writes the generated SDK-owned module entrypoint under `bin/<module.exec>`. */
|
|
29
|
-
export declare function generateModuleBootstrap(pluginDir: string, moduleExec: string | undefined): void;
|
|
29
|
+
export declare function generateModuleBootstrap(pluginDir: string, moduleExec: string | undefined, verbose?: boolean): void;
|
|
30
30
|
/** Returns whether the plugin repository has a `package.json`. */
|
|
31
31
|
export declare function hasPackageJson(pluginDir: string): boolean;
|
|
32
32
|
/** Runs a command in the given directory with optional verbose logging. */
|
package/lib/build.js
CHANGED
|
@@ -54,6 +54,9 @@ const fs_utils_1 = require("./fs-utils");
|
|
|
54
54
|
const npm_runner_1 = require("./npm-runner");
|
|
55
55
|
var npm_runner_2 = require("./npm-runner");
|
|
56
56
|
Object.defineProperty(exports, "npmExecutable", { enumerable: true, get: function () { return npm_runner_2.npmExecutable; } });
|
|
57
|
+
function writeBuildProgress(message) {
|
|
58
|
+
process.stderr.write(`openvcs build: ${message}\n`);
|
|
59
|
+
}
|
|
57
60
|
const AUTHORED_PLUGIN_MODULE_BASENAME = "plugin.js";
|
|
58
61
|
/** Formats help text for the build command. */
|
|
59
62
|
function buildUsage(commandName = "openvcs") {
|
|
@@ -90,11 +93,14 @@ function parseBuildArgs(args) {
|
|
|
90
93
|
};
|
|
91
94
|
}
|
|
92
95
|
/** Reads and validates the plugin manifest. */
|
|
93
|
-
function readManifest(pluginDir) {
|
|
96
|
+
function readManifest(pluginDir, verbose = false) {
|
|
94
97
|
const manifestPath = path.join(pluginDir, "package.json");
|
|
95
98
|
let manifestRaw;
|
|
96
99
|
let manifestFd;
|
|
97
100
|
let manifest;
|
|
101
|
+
if (verbose) {
|
|
102
|
+
writeBuildProgress(`reading manifest at ${manifestPath}`);
|
|
103
|
+
}
|
|
98
104
|
try {
|
|
99
105
|
manifestFd = fs.openSync(manifestPath, "r");
|
|
100
106
|
const manifestStat = fs.fstatSync(manifestFd);
|
|
@@ -138,6 +144,9 @@ function readManifest(pluginDir) {
|
|
|
138
144
|
const moduleExec = typeof moduleValue?.exec === "string" ? moduleValue.exec.trim() : undefined;
|
|
139
145
|
const entryValue = openvcs.entry;
|
|
140
146
|
const entry = typeof entryValue === "string" ? entryValue.trim() : undefined;
|
|
147
|
+
if (verbose) {
|
|
148
|
+
writeBuildProgress(`manifest loaded for ${pluginId}${moduleExec ? ` (module.exec: ${moduleExec})` : ""}`);
|
|
149
|
+
}
|
|
141
150
|
return {
|
|
142
151
|
pluginId,
|
|
143
152
|
moduleExec,
|
|
@@ -146,11 +155,14 @@ function readManifest(pluginDir) {
|
|
|
146
155
|
};
|
|
147
156
|
}
|
|
148
157
|
/** Verifies that a declared module entry resolves to a real file under `bin/`. */
|
|
149
|
-
function validateDeclaredModuleExec(pluginDir, moduleExec) {
|
|
158
|
+
function validateDeclaredModuleExec(pluginDir, moduleExec, verbose = false) {
|
|
150
159
|
if (!moduleExec) {
|
|
151
160
|
return;
|
|
152
161
|
}
|
|
153
162
|
const targetPath = resolveDeclaredModuleExecPath(pluginDir, moduleExec);
|
|
163
|
+
if (verbose) {
|
|
164
|
+
writeBuildProgress(`checking declared bootstrap at ${targetPath}`);
|
|
165
|
+
}
|
|
154
166
|
if (!fs.existsSync(targetPath) || !fs.lstatSync(targetPath).isFile()) {
|
|
155
167
|
throw new Error(`module entrypoint not found at ${targetPath}`);
|
|
156
168
|
}
|
|
@@ -177,10 +189,13 @@ function authoredPluginModulePath(pluginDir) {
|
|
|
177
189
|
return path.resolve(pluginDir, "bin", AUTHORED_PLUGIN_MODULE_BASENAME);
|
|
178
190
|
}
|
|
179
191
|
/** Ensures the plugin's authored module and generated bootstrap paths are compatible. */
|
|
180
|
-
function validateGeneratedBootstrapTargets(pluginDir, moduleExec) {
|
|
192
|
+
function validateGeneratedBootstrapTargets(pluginDir, moduleExec, verbose = false) {
|
|
181
193
|
if (!moduleExec) {
|
|
182
194
|
return;
|
|
183
195
|
}
|
|
196
|
+
if (verbose) {
|
|
197
|
+
writeBuildProgress(`validating generated bootstrap target for ${moduleExec}`);
|
|
198
|
+
}
|
|
184
199
|
resolveDeclaredModuleExecPath(pluginDir, moduleExec);
|
|
185
200
|
const normalizedExec = moduleExec.trim().toLowerCase();
|
|
186
201
|
if (normalizedExec === AUTHORED_PLUGIN_MODULE_BASENAME.toLowerCase()) {
|
|
@@ -211,16 +226,21 @@ function renderGeneratedBootstrap(pluginModuleImportPath, isEsm) {
|
|
|
211
226
|
return `#!/usr/bin/env node\n// Copyright © 2025-2026 OpenVCS Contributors\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n(async () => {\n const { bootstrapPluginModule } = require('@openvcs/sdk/runtime');\n await bootstrapPluginModule({\n importPluginModule: async () => require('${pluginModuleImportPath}'),\n modulePath: '${pluginModuleImportPath}',\n });\n})();\n`;
|
|
212
227
|
}
|
|
213
228
|
/** Writes the generated SDK-owned module entrypoint under `bin/<module.exec>`. */
|
|
214
|
-
function generateModuleBootstrap(pluginDir, moduleExec) {
|
|
229
|
+
function generateModuleBootstrap(pluginDir, moduleExec, verbose = false) {
|
|
215
230
|
if (!moduleExec) {
|
|
216
|
-
|
|
231
|
+
if (verbose) {
|
|
232
|
+
writeBuildProgress(`no module.exec defined for ${pluginDir}; skipping bootstrap generation`);
|
|
233
|
+
}
|
|
217
234
|
return;
|
|
218
235
|
}
|
|
219
|
-
validateGeneratedBootstrapTargets(pluginDir, moduleExec);
|
|
236
|
+
validateGeneratedBootstrapTargets(pluginDir, moduleExec, verbose);
|
|
220
237
|
const execPath = resolveDeclaredModuleExecPath(pluginDir, moduleExec);
|
|
221
238
|
const pluginModulePath = authoredPluginModulePath(pluginDir);
|
|
222
239
|
const pluginModuleImportPath = relativeBinImport(execPath, pluginModulePath);
|
|
223
240
|
const isEsm = detectEsmMode(pluginDir, moduleExec);
|
|
241
|
+
if (verbose) {
|
|
242
|
+
writeBuildProgress(`writing bootstrap ${execPath} -> ${pluginModuleImportPath}${isEsm ? " (esm)" : " (cjs)"}`);
|
|
243
|
+
}
|
|
224
244
|
fs.mkdirSync(path.dirname(execPath), { recursive: true });
|
|
225
245
|
fs.writeFileSync(execPath, renderGeneratedBootstrap(pluginModuleImportPath, isEsm), "utf8");
|
|
226
246
|
}
|
|
@@ -285,10 +305,13 @@ function readPackageScripts(pluginDir) {
|
|
|
285
305
|
}
|
|
286
306
|
/** Builds a plugin's runtime assets when it declares a code module. */
|
|
287
307
|
function buildPluginAssets(parsedArgs) {
|
|
288
|
-
|
|
308
|
+
writeBuildProgress(`reading plugin manifest in ${parsedArgs.pluginDir}`);
|
|
309
|
+
const manifest = readManifest(parsedArgs.pluginDir, parsedArgs.verbose);
|
|
289
310
|
if (!manifest.moduleExec) {
|
|
311
|
+
writeBuildProgress(`theme-only plugin ${manifest.pluginId}; nothing to build`);
|
|
290
312
|
return manifest;
|
|
291
313
|
}
|
|
314
|
+
writeBuildProgress(`building plugin ${manifest.pluginId}`);
|
|
292
315
|
if (!hasPackageJson(parsedArgs.pluginDir)) {
|
|
293
316
|
throw new Error(`code plugins must include package.json: ${path.join(parsedArgs.pluginDir, "package.json")}`);
|
|
294
317
|
}
|
|
@@ -297,8 +320,12 @@ function buildPluginAssets(parsedArgs) {
|
|
|
297
320
|
if (typeof buildScript !== "string" || buildScript.trim() === "") {
|
|
298
321
|
throw new Error(`code plugins must define scripts[\"build:plugin\"] in ${path.join(parsedArgs.pluginDir, "package.json")}`);
|
|
299
322
|
}
|
|
323
|
+
writeBuildProgress(`running build:plugin`);
|
|
300
324
|
runCommand((0, npm_runner_1.npmExecutable)(), [...(0, npm_runner_1.npmArgsPrefix)(), "run", "build:plugin"], parsedArgs.pluginDir, parsedArgs.verbose);
|
|
301
|
-
|
|
302
|
-
|
|
325
|
+
writeBuildProgress(`generating bootstrap ${manifest.moduleExec}`);
|
|
326
|
+
generateModuleBootstrap(parsedArgs.pluginDir, manifest.moduleExec, parsedArgs.verbose);
|
|
327
|
+
writeBuildProgress(`validating bootstrap ${manifest.moduleExec}`);
|
|
328
|
+
validateDeclaredModuleExec(parsedArgs.pluginDir, manifest.moduleExec, parsedArgs.verbose);
|
|
329
|
+
writeBuildProgress(`build complete for ${manifest.pluginId}`);
|
|
303
330
|
return manifest;
|
|
304
331
|
}
|
|
@@ -66,7 +66,7 @@ export declare abstract class VcsDelegateBase<TDeps, TContext = PluginRuntimeCon
|
|
|
66
66
|
/** Handles `vcs.list_commits`. */
|
|
67
67
|
listCommits(_params: VcsTypes.VcsListCommitsParams, _context: TContext): VcsHandlerResult<VcsTypes.CommitEntry[]>;
|
|
68
68
|
/** Handles `vcs.diff_file`. */
|
|
69
|
-
diffFile(_params: VcsTypes.VcsDiffFileParams, _context: TContext): VcsHandlerResult<
|
|
69
|
+
diffFile(_params: VcsTypes.VcsDiffFileParams, _context: TContext): VcsHandlerResult<VcsTypes.VcsDiffFileResponse>;
|
|
70
70
|
/** Handles `vcs.diff_commit`. */
|
|
71
71
|
diffCommit(_params: VcsTypes.VcsDiffCommitParams, _context: TContext): VcsHandlerResult<string[]>;
|
|
72
72
|
/** Handles `vcs.get_conflict_details`. */
|
|
@@ -77,6 +77,8 @@ export declare abstract class VcsDelegateBase<TDeps, TContext = PluginRuntimeCon
|
|
|
77
77
|
writeMergeResult(_params: VcsTypes.VcsWriteMergeResultParams, _context: TContext): VcsHandlerResult<null>;
|
|
78
78
|
/** Handles `vcs.stage_patch`. */
|
|
79
79
|
stagePatch(_params: VcsTypes.VcsStagePatchParams, _context: TContext): VcsHandlerResult<null>;
|
|
80
|
+
/** Handles `vcs.stage_selections` (structured hunk/line selections). */
|
|
81
|
+
stageSelections(_params: VcsTypes.VcsStageSelectionsParams, _context: TContext): VcsHandlerResult<null>;
|
|
80
82
|
/** Handles `vcs.stage_paths`. */
|
|
81
83
|
stagePaths(_params: VcsTypes.VcsStagePathsParams, _context: TContext): VcsHandlerResult<null>;
|
|
82
84
|
/** Handles `vcs.discard_paths`. */
|
|
@@ -157,6 +157,10 @@ class VcsDelegateBase {
|
|
|
157
157
|
stagePatch(_params, _context) {
|
|
158
158
|
return this.unimplemented('stagePatch');
|
|
159
159
|
}
|
|
160
|
+
/** Handles `vcs.stage_selections` (structured hunk/line selections). */
|
|
161
|
+
stageSelections(_params, _context) {
|
|
162
|
+
return this.unimplemented('stageSelections');
|
|
163
|
+
}
|
|
160
164
|
/** Handles `vcs.stage_paths`. */
|
|
161
165
|
stagePaths(_params, _context) {
|
|
162
166
|
return this.unimplemented('stagePaths');
|
|
@@ -30,6 +30,7 @@ export type VcsDelegateBindings<TContext> = {
|
|
|
30
30
|
checkoutConflictSide: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.checkout_conflict_side']>;
|
|
31
31
|
writeMergeResult: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.write_merge_result']>;
|
|
32
32
|
stagePatch: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.stage_patch']>;
|
|
33
|
+
stageSelections: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.stage_selections']>;
|
|
33
34
|
stagePaths: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.stage_paths']>;
|
|
34
35
|
discardPaths: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.discard_paths']>;
|
|
35
36
|
applyReversePatch: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.apply_reverse_patch']>;
|
|
@@ -86,6 +87,7 @@ export declare const VCS_DELEGATE_METHOD_MAPPINGS: {
|
|
|
86
87
|
readonly checkoutConflictSide: "vcs.checkout_conflict_side";
|
|
87
88
|
readonly writeMergeResult: "vcs.write_merge_result";
|
|
88
89
|
readonly stagePatch: "vcs.stage_patch";
|
|
90
|
+
readonly stageSelections: "vcs.stage_selections";
|
|
89
91
|
readonly stagePaths: "vcs.stage_paths";
|
|
90
92
|
readonly discardPaths: "vcs.discard_paths";
|
|
91
93
|
readonly applyReversePatch: "vcs.apply_reverse_patch";
|
|
@@ -33,6 +33,7 @@ exports.VCS_DELEGATE_METHOD_MAPPINGS = {
|
|
|
33
33
|
checkoutConflictSide: 'vcs.checkout_conflict_side',
|
|
34
34
|
writeMergeResult: 'vcs.write_merge_result',
|
|
35
35
|
stagePatch: 'vcs.stage_patch',
|
|
36
|
+
stageSelections: 'vcs.stage_selections',
|
|
36
37
|
stagePaths: 'vcs.stage_paths',
|
|
37
38
|
discardPaths: 'vcs.discard_paths',
|
|
38
39
|
applyReversePatch: 'vcs.apply_reverse_patch',
|
package/lib/types/vcs.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface VcsCapabilities {
|
|
|
13
13
|
push_pull: boolean;
|
|
14
14
|
/** Indicates whether fast-forward helpers are supported. */
|
|
15
15
|
fast_forward: boolean;
|
|
16
|
+
/** Merge strategies the backend supports (values like "merge", "squash", "rebase"). */
|
|
17
|
+
merge_strategies?: string[];
|
|
16
18
|
}
|
|
17
19
|
/** Describes params that carry a repository session id. */
|
|
18
20
|
export interface VcsSessionParams extends RequestParams {
|
|
@@ -92,19 +94,12 @@ export interface VcsRemoveRemoteParams extends VcsSessionParams {
|
|
|
92
94
|
/** Stores the remote name to remove. */
|
|
93
95
|
name: string;
|
|
94
96
|
}
|
|
95
|
-
/** Describes optional fetch flags. */
|
|
96
|
-
export interface VcsFetchOptions {
|
|
97
|
-
/** Indicates whether stale remote references should be pruned. */
|
|
98
|
-
prune?: boolean;
|
|
99
|
-
}
|
|
100
97
|
/** Describes params for fetch methods. */
|
|
101
98
|
export interface VcsFetchParams extends VcsSessionParams {
|
|
102
99
|
/** Stores the remote to fetch when one is supplied. */
|
|
103
100
|
remote?: string;
|
|
104
101
|
/** Stores the refspec to fetch when one is supplied. */
|
|
105
102
|
refspec?: string;
|
|
106
|
-
/** Stores optional fetch flags. */
|
|
107
|
-
opts?: VcsFetchOptions;
|
|
108
103
|
}
|
|
109
104
|
/** Describes params for push. */
|
|
110
105
|
export interface VcsPushParams extends VcsSessionParams {
|
|
@@ -156,6 +151,8 @@ export interface StatusFileEntry {
|
|
|
156
151
|
resolved_conflict: boolean;
|
|
157
152
|
/** Stores placeholder hunk information until richer diff support exists. */
|
|
158
153
|
hunks: never[];
|
|
154
|
+
/** Indicates whether the file content should be treated as binary. */
|
|
155
|
+
binary?: boolean | null;
|
|
159
156
|
}
|
|
160
157
|
/** Describes the structured status payload returned to the host. */
|
|
161
158
|
export interface StatusPayload {
|
|
@@ -224,6 +221,15 @@ export interface VcsDiffCommitParams extends VcsSessionParams {
|
|
|
224
221
|
/** Stores the revision to diff. */
|
|
225
222
|
rev: string;
|
|
226
223
|
}
|
|
224
|
+
/** Describes one structured diff payload returned for a file. */
|
|
225
|
+
export interface VcsDiffResult {
|
|
226
|
+
/** Stores line-oriented diff output. */
|
|
227
|
+
lines: string[];
|
|
228
|
+
/** Indicates whether the diff target should be treated as binary. */
|
|
229
|
+
binary?: boolean | null;
|
|
230
|
+
}
|
|
231
|
+
/** Describes the accepted `vcs.diff_file` response shapes. */
|
|
232
|
+
export type VcsDiffFileResponse = VcsDiffResult | string[];
|
|
227
233
|
/** Describes params for `vcs.get_conflict_details`. */
|
|
228
234
|
export interface VcsGetConflictDetailsParams extends VcsSessionParams {
|
|
229
235
|
/** Stores the conflicted path. */
|
|
@@ -241,8 +247,6 @@ export interface VcsConflictDetails {
|
|
|
241
247
|
theirs: string | null;
|
|
242
248
|
/** Indicates whether the conflict is binary. */
|
|
243
249
|
binary: boolean;
|
|
244
|
-
/** Indicates whether the conflict references Git LFS content. */
|
|
245
|
-
lfs_pointer: boolean;
|
|
246
250
|
}
|
|
247
251
|
/** Describes params for checking out one side of a conflict. */
|
|
248
252
|
export interface VcsCheckoutConflictSideParams extends VcsSessionParams {
|
|
@@ -263,6 +267,20 @@ export interface VcsStagePatchParams extends VcsSessionParams {
|
|
|
263
267
|
/** Stores the textual patch content. */
|
|
264
268
|
patch: string;
|
|
265
269
|
}
|
|
270
|
+
/** Describes a single file's hunk/line selection for partial staging. */
|
|
271
|
+
export interface HunkSelection {
|
|
272
|
+
/** Repository-relative file path. */
|
|
273
|
+
path: string;
|
|
274
|
+
/** Indices of whole hunks to include. */
|
|
275
|
+
whole_hunks: number[];
|
|
276
|
+
/** Per-hunk line selections: maps hunk index → 1-based line offsets. */
|
|
277
|
+
partial_hunks: Record<number, number[]>;
|
|
278
|
+
}
|
|
279
|
+
/** Describes params for staging structured selections (VCS-agnostic). */
|
|
280
|
+
export interface VcsStageSelectionsParams extends VcsSessionParams {
|
|
281
|
+
/** Structured hunk/line selections for multiple files. */
|
|
282
|
+
selections: HunkSelection[];
|
|
283
|
+
}
|
|
266
284
|
/** Describes params for staging repository-relative paths into the index. */
|
|
267
285
|
export interface VcsStagePathsParams extends VcsSessionParams {
|
|
268
286
|
/** Stores the paths to stage. */
|
|
@@ -292,12 +310,16 @@ export interface VcsRenameBranchParams extends VcsSessionParams {
|
|
|
292
310
|
/** Stores the next branch name. */
|
|
293
311
|
new: string;
|
|
294
312
|
}
|
|
313
|
+
/** Describes the supported merge strategies. */
|
|
314
|
+
export type VcsMergeStrategy = 'merge' | 'squash' | 'rebase';
|
|
295
315
|
/** Describes params for merging another branch into the current branch. */
|
|
296
316
|
export interface VcsMergeIntoCurrentParams extends VcsSessionParams {
|
|
297
317
|
/** Stores the branch name to merge. */
|
|
298
318
|
name: string;
|
|
299
319
|
/** Stores an optional merge commit message. */
|
|
300
320
|
message?: string;
|
|
321
|
+
/** Stores the merge strategy. Defaults to 'merge' when absent. */
|
|
322
|
+
strategy?: VcsMergeStrategy;
|
|
301
323
|
}
|
|
302
324
|
/** Describes params for setting a branch upstream. */
|
|
303
325
|
export interface VcsSetBranchUpstreamParams extends VcsSessionParams {
|
|
@@ -420,7 +442,7 @@ export interface VcsDelegates<TContext = unknown> {
|
|
|
420
442
|
/** Handles `vcs.list_commits`. */
|
|
421
443
|
'vcs.list_commits'?: RpcMethodHandler<VcsListCommitsParams, CommitEntry[], TContext>;
|
|
422
444
|
/** Handles `vcs.diff_file`. */
|
|
423
|
-
'vcs.diff_file'?: RpcMethodHandler<VcsDiffFileParams,
|
|
445
|
+
'vcs.diff_file'?: RpcMethodHandler<VcsDiffFileParams, VcsDiffFileResponse, TContext>;
|
|
424
446
|
/** Handles `vcs.diff_commit`. */
|
|
425
447
|
'vcs.diff_commit'?: RpcMethodHandler<VcsDiffCommitParams, string[], TContext>;
|
|
426
448
|
/** Handles `vcs.get_conflict_details`. */
|
|
@@ -431,6 +453,8 @@ export interface VcsDelegates<TContext = unknown> {
|
|
|
431
453
|
'vcs.write_merge_result'?: RpcMethodHandler<VcsWriteMergeResultParams, null, TContext>;
|
|
432
454
|
/** Handles `vcs.stage_patch`. */
|
|
433
455
|
'vcs.stage_patch'?: RpcMethodHandler<VcsStagePatchParams, null, TContext>;
|
|
456
|
+
/** Handles `vcs.stage_selections` (structured hunk/line selections). */
|
|
457
|
+
'vcs.stage_selections'?: RpcMethodHandler<VcsStageSelectionsParams, null, TContext>;
|
|
434
458
|
/** Handles `vcs.stage_paths`. */
|
|
435
459
|
'vcs.stage_paths'?: RpcMethodHandler<VcsStagePathsParams, null, TContext>;
|
|
436
460
|
/** Handles `vcs.discard_paths`. */
|
package/package.json
CHANGED
package/src/lib/build.ts
CHANGED
|
@@ -26,6 +26,10 @@ export interface ManifestInfo {
|
|
|
26
26
|
manifestPath: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function writeBuildProgress(message: string): void {
|
|
30
|
+
process.stderr.write(`openvcs build: ${message}\n`);
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
interface CommandResult {
|
|
30
34
|
status: number | null;
|
|
31
35
|
error?: Error;
|
|
@@ -76,11 +80,14 @@ export function parseBuildArgs(args: string[]): BuildArgs {
|
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
/** Reads and validates the plugin manifest. */
|
|
79
|
-
export function readManifest(pluginDir: string): ManifestInfo {
|
|
83
|
+
export function readManifest(pluginDir: string, verbose = false): ManifestInfo {
|
|
80
84
|
const manifestPath = path.join(pluginDir, "package.json");
|
|
81
85
|
let manifestRaw: string;
|
|
82
86
|
let manifestFd: number | undefined;
|
|
83
87
|
let manifest: unknown;
|
|
88
|
+
if (verbose) {
|
|
89
|
+
writeBuildProgress(`reading manifest at ${manifestPath}`);
|
|
90
|
+
}
|
|
84
91
|
try {
|
|
85
92
|
manifestFd = fs.openSync(manifestPath, "r");
|
|
86
93
|
const manifestStat = fs.fstatSync(manifestFd);
|
|
@@ -130,6 +137,10 @@ export function readManifest(pluginDir: string): ManifestInfo {
|
|
|
130
137
|
const entryValue = openvcs.entry;
|
|
131
138
|
const entry = typeof entryValue === "string" ? entryValue.trim() : undefined;
|
|
132
139
|
|
|
140
|
+
if (verbose) {
|
|
141
|
+
writeBuildProgress(`manifest loaded for ${pluginId}${moduleExec ? ` (module.exec: ${moduleExec})` : ""}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
133
144
|
return {
|
|
134
145
|
pluginId,
|
|
135
146
|
moduleExec,
|
|
@@ -139,12 +150,19 @@ export function readManifest(pluginDir: string): ManifestInfo {
|
|
|
139
150
|
}
|
|
140
151
|
|
|
141
152
|
/** Verifies that a declared module entry resolves to a real file under `bin/`. */
|
|
142
|
-
export function validateDeclaredModuleExec(
|
|
153
|
+
export function validateDeclaredModuleExec(
|
|
154
|
+
pluginDir: string,
|
|
155
|
+
moduleExec: string | undefined,
|
|
156
|
+
verbose = false,
|
|
157
|
+
): void {
|
|
143
158
|
if (!moduleExec) {
|
|
144
159
|
return;
|
|
145
160
|
}
|
|
146
161
|
|
|
147
162
|
const targetPath = resolveDeclaredModuleExecPath(pluginDir, moduleExec);
|
|
163
|
+
if (verbose) {
|
|
164
|
+
writeBuildProgress(`checking declared bootstrap at ${targetPath}`);
|
|
165
|
+
}
|
|
148
166
|
if (!fs.existsSync(targetPath) || !fs.lstatSync(targetPath).isFile()) {
|
|
149
167
|
throw new Error(`module entrypoint not found at ${targetPath}`);
|
|
150
168
|
}
|
|
@@ -179,11 +197,15 @@ export function authoredPluginModulePath(pluginDir: string): string {
|
|
|
179
197
|
export function validateGeneratedBootstrapTargets(
|
|
180
198
|
pluginDir: string,
|
|
181
199
|
moduleExec: string | undefined,
|
|
200
|
+
verbose = false,
|
|
182
201
|
): void {
|
|
183
202
|
if (!moduleExec) {
|
|
184
203
|
return;
|
|
185
204
|
}
|
|
186
205
|
|
|
206
|
+
if (verbose) {
|
|
207
|
+
writeBuildProgress(`validating generated bootstrap target for ${moduleExec}`);
|
|
208
|
+
}
|
|
187
209
|
resolveDeclaredModuleExecPath(pluginDir, moduleExec);
|
|
188
210
|
const normalizedExec = moduleExec.trim().toLowerCase();
|
|
189
211
|
if (normalizedExec === AUTHORED_PLUGIN_MODULE_BASENAME.toLowerCase()) {
|
|
@@ -230,18 +252,28 @@ export function renderGeneratedBootstrap(
|
|
|
230
252
|
}
|
|
231
253
|
|
|
232
254
|
/** Writes the generated SDK-owned module entrypoint under `bin/<module.exec>`. */
|
|
233
|
-
export function generateModuleBootstrap(
|
|
255
|
+
export function generateModuleBootstrap(
|
|
256
|
+
pluginDir: string,
|
|
257
|
+
moduleExec: string | undefined,
|
|
258
|
+
verbose = false,
|
|
259
|
+
): void {
|
|
234
260
|
if (!moduleExec) {
|
|
235
|
-
|
|
261
|
+
if (verbose) {
|
|
262
|
+
writeBuildProgress(`no module.exec defined for ${pluginDir}; skipping bootstrap generation`);
|
|
263
|
+
}
|
|
236
264
|
return;
|
|
237
265
|
}
|
|
238
266
|
|
|
239
|
-
validateGeneratedBootstrapTargets(pluginDir, moduleExec);
|
|
267
|
+
validateGeneratedBootstrapTargets(pluginDir, moduleExec, verbose);
|
|
240
268
|
const execPath = resolveDeclaredModuleExecPath(pluginDir, moduleExec);
|
|
241
269
|
const pluginModulePath = authoredPluginModulePath(pluginDir);
|
|
242
270
|
const pluginModuleImportPath = relativeBinImport(execPath, pluginModulePath);
|
|
243
271
|
const isEsm = detectEsmMode(pluginDir, moduleExec);
|
|
244
272
|
|
|
273
|
+
if (verbose) {
|
|
274
|
+
writeBuildProgress(`writing bootstrap ${execPath} -> ${pluginModuleImportPath}${isEsm ? " (esm)" : " (cjs)"}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
245
277
|
fs.mkdirSync(path.dirname(execPath), { recursive: true });
|
|
246
278
|
fs.writeFileSync(execPath, renderGeneratedBootstrap(pluginModuleImportPath, isEsm), "utf8");
|
|
247
279
|
}
|
|
@@ -313,11 +345,14 @@ function readPackageScripts(pluginDir: string): PackageScripts {
|
|
|
313
345
|
|
|
314
346
|
/** Builds a plugin's runtime assets when it declares a code module. */
|
|
315
347
|
export function buildPluginAssets(parsedArgs: BuildArgs): ManifestInfo {
|
|
316
|
-
|
|
348
|
+
writeBuildProgress(`reading plugin manifest in ${parsedArgs.pluginDir}`);
|
|
349
|
+
const manifest = readManifest(parsedArgs.pluginDir, parsedArgs.verbose);
|
|
317
350
|
if (!manifest.moduleExec) {
|
|
351
|
+
writeBuildProgress(`theme-only plugin ${manifest.pluginId}; nothing to build`);
|
|
318
352
|
return manifest;
|
|
319
353
|
}
|
|
320
354
|
|
|
355
|
+
writeBuildProgress(`building plugin ${manifest.pluginId}`);
|
|
321
356
|
if (!hasPackageJson(parsedArgs.pluginDir)) {
|
|
322
357
|
throw new Error(`code plugins must include package.json: ${path.join(parsedArgs.pluginDir, "package.json")}`);
|
|
323
358
|
}
|
|
@@ -330,8 +365,12 @@ export function buildPluginAssets(parsedArgs: BuildArgs): ManifestInfo {
|
|
|
330
365
|
);
|
|
331
366
|
}
|
|
332
367
|
|
|
368
|
+
writeBuildProgress(`running build:plugin`);
|
|
333
369
|
runCommand(npmExecutable(), [...npmArgsPrefix(), "run", "build:plugin"], parsedArgs.pluginDir, parsedArgs.verbose);
|
|
334
|
-
|
|
335
|
-
|
|
370
|
+
writeBuildProgress(`generating bootstrap ${manifest.moduleExec}`);
|
|
371
|
+
generateModuleBootstrap(parsedArgs.pluginDir, manifest.moduleExec, parsedArgs.verbose);
|
|
372
|
+
writeBuildProgress(`validating bootstrap ${manifest.moduleExec}`);
|
|
373
|
+
validateDeclaredModuleExec(parsedArgs.pluginDir, manifest.moduleExec, parsedArgs.verbose);
|
|
374
|
+
writeBuildProgress(`build complete for ${manifest.pluginId}`);
|
|
336
375
|
return manifest;
|
|
337
376
|
}
|
|
@@ -259,7 +259,7 @@ export abstract class VcsDelegateBase<
|
|
|
259
259
|
diffFile(
|
|
260
260
|
_params: VcsTypes.VcsDiffFileParams,
|
|
261
261
|
_context: TContext,
|
|
262
|
-
): VcsHandlerResult<
|
|
262
|
+
): VcsHandlerResult<VcsTypes.VcsDiffFileResponse> {
|
|
263
263
|
return this.unimplemented('diffFile');
|
|
264
264
|
}
|
|
265
265
|
|
|
@@ -303,6 +303,14 @@ export abstract class VcsDelegateBase<
|
|
|
303
303
|
return this.unimplemented('stagePatch');
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
/** Handles `vcs.stage_selections` (structured hunk/line selections). */
|
|
307
|
+
stageSelections(
|
|
308
|
+
_params: VcsTypes.VcsStageSelectionsParams,
|
|
309
|
+
_context: TContext,
|
|
310
|
+
): VcsHandlerResult<null> {
|
|
311
|
+
return this.unimplemented('stageSelections');
|
|
312
|
+
}
|
|
313
|
+
|
|
306
314
|
/** Handles `vcs.stage_paths`. */
|
|
307
315
|
stagePaths(
|
|
308
316
|
_params: VcsTypes.VcsStagePathsParams,
|
|
@@ -53,6 +53,7 @@ export type VcsDelegateBindings<TContext> = {
|
|
|
53
53
|
VcsTypes.VcsDelegates<TContext>['vcs.write_merge_result']
|
|
54
54
|
>;
|
|
55
55
|
stagePatch: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.stage_patch']>;
|
|
56
|
+
stageSelections: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.stage_selections']>;
|
|
56
57
|
stagePaths: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.stage_paths']>;
|
|
57
58
|
discardPaths: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.discard_paths']>;
|
|
58
59
|
applyReversePatch: NonNullable<
|
|
@@ -123,6 +124,7 @@ export const VCS_DELEGATE_METHOD_MAPPINGS = {
|
|
|
123
124
|
checkoutConflictSide: 'vcs.checkout_conflict_side',
|
|
124
125
|
writeMergeResult: 'vcs.write_merge_result',
|
|
125
126
|
stagePatch: 'vcs.stage_patch',
|
|
127
|
+
stageSelections: 'vcs.stage_selections',
|
|
126
128
|
stagePaths: 'vcs.stage_paths',
|
|
127
129
|
discardPaths: 'vcs.discard_paths',
|
|
128
130
|
applyReversePatch: 'vcs.apply_reverse_patch',
|
package/src/lib/types/vcs.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface VcsCapabilities {
|
|
|
17
17
|
push_pull: boolean;
|
|
18
18
|
/** Indicates whether fast-forward helpers are supported. */
|
|
19
19
|
fast_forward: boolean;
|
|
20
|
+
/** Merge strategies the backend supports (values like "merge", "squash", "rebase"). */
|
|
21
|
+
merge_strategies?: string[];
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/** Describes params that carry a repository session id. */
|
|
@@ -110,20 +112,13 @@ export interface VcsRemoveRemoteParams extends VcsSessionParams {
|
|
|
110
112
|
name: string;
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
/** Describes optional fetch flags. */
|
|
114
|
-
export interface VcsFetchOptions {
|
|
115
|
-
/** Indicates whether stale remote references should be pruned. */
|
|
116
|
-
prune?: boolean;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
115
|
/** Describes params for fetch methods. */
|
|
116
|
+
|
|
120
117
|
export interface VcsFetchParams extends VcsSessionParams {
|
|
121
118
|
/** Stores the remote to fetch when one is supplied. */
|
|
122
119
|
remote?: string;
|
|
123
120
|
/** Stores the refspec to fetch when one is supplied. */
|
|
124
121
|
refspec?: string;
|
|
125
|
-
/** Stores optional fetch flags. */
|
|
126
|
-
opts?: VcsFetchOptions;
|
|
127
122
|
}
|
|
128
123
|
|
|
129
124
|
/** Describes params for push. */
|
|
@@ -180,6 +175,8 @@ export interface StatusFileEntry {
|
|
|
180
175
|
resolved_conflict: boolean;
|
|
181
176
|
/** Stores placeholder hunk information until richer diff support exists. */
|
|
182
177
|
hunks: never[];
|
|
178
|
+
/** Indicates whether the file content should be treated as binary. */
|
|
179
|
+
binary?: boolean | null;
|
|
183
180
|
}
|
|
184
181
|
|
|
185
182
|
/** Describes the structured status payload returned to the host. */
|
|
@@ -256,6 +253,17 @@ export interface VcsDiffCommitParams extends VcsSessionParams {
|
|
|
256
253
|
rev: string;
|
|
257
254
|
}
|
|
258
255
|
|
|
256
|
+
/** Describes one structured diff payload returned for a file. */
|
|
257
|
+
export interface VcsDiffResult {
|
|
258
|
+
/** Stores line-oriented diff output. */
|
|
259
|
+
lines: string[];
|
|
260
|
+
/** Indicates whether the diff target should be treated as binary. */
|
|
261
|
+
binary?: boolean | null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Describes the accepted `vcs.diff_file` response shapes. */
|
|
265
|
+
export type VcsDiffFileResponse = VcsDiffResult | string[];
|
|
266
|
+
|
|
259
267
|
/** Describes params for `vcs.get_conflict_details`. */
|
|
260
268
|
export interface VcsGetConflictDetailsParams extends VcsSessionParams {
|
|
261
269
|
/** Stores the conflicted path. */
|
|
@@ -274,8 +282,6 @@ export interface VcsConflictDetails {
|
|
|
274
282
|
theirs: string | null;
|
|
275
283
|
/** Indicates whether the conflict is binary. */
|
|
276
284
|
binary: boolean;
|
|
277
|
-
/** Indicates whether the conflict references Git LFS content. */
|
|
278
|
-
lfs_pointer: boolean;
|
|
279
285
|
}
|
|
280
286
|
|
|
281
287
|
/** Describes params for checking out one side of a conflict. */
|
|
@@ -300,6 +306,22 @@ export interface VcsStagePatchParams extends VcsSessionParams {
|
|
|
300
306
|
patch: string;
|
|
301
307
|
}
|
|
302
308
|
|
|
309
|
+
/** Describes a single file's hunk/line selection for partial staging. */
|
|
310
|
+
export interface HunkSelection {
|
|
311
|
+
/** Repository-relative file path. */
|
|
312
|
+
path: string;
|
|
313
|
+
/** Indices of whole hunks to include. */
|
|
314
|
+
whole_hunks: number[];
|
|
315
|
+
/** Per-hunk line selections: maps hunk index → 1-based line offsets. */
|
|
316
|
+
partial_hunks: Record<number, number[]>;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Describes params for staging structured selections (VCS-agnostic). */
|
|
320
|
+
export interface VcsStageSelectionsParams extends VcsSessionParams {
|
|
321
|
+
/** Structured hunk/line selections for multiple files. */
|
|
322
|
+
selections: HunkSelection[];
|
|
323
|
+
}
|
|
324
|
+
|
|
303
325
|
/** Describes params for staging repository-relative paths into the index. */
|
|
304
326
|
export interface VcsStagePathsParams extends VcsSessionParams {
|
|
305
327
|
/** Stores the paths to stage. */
|
|
@@ -334,12 +356,17 @@ export interface VcsRenameBranchParams extends VcsSessionParams {
|
|
|
334
356
|
new: string;
|
|
335
357
|
}
|
|
336
358
|
|
|
359
|
+
/** Describes the supported merge strategies. */
|
|
360
|
+
export type VcsMergeStrategy = 'merge' | 'squash' | 'rebase';
|
|
361
|
+
|
|
337
362
|
/** Describes params for merging another branch into the current branch. */
|
|
338
363
|
export interface VcsMergeIntoCurrentParams extends VcsSessionParams {
|
|
339
364
|
/** Stores the branch name to merge. */
|
|
340
365
|
name: string;
|
|
341
366
|
/** Stores an optional merge commit message. */
|
|
342
367
|
message?: string;
|
|
368
|
+
/** Stores the merge strategy. Defaults to 'merge' when absent. */
|
|
369
|
+
strategy?: VcsMergeStrategy;
|
|
343
370
|
}
|
|
344
371
|
|
|
345
372
|
/** Describes params for setting a branch upstream. */
|
|
@@ -503,7 +530,7 @@ export interface VcsDelegates<TContext = unknown> {
|
|
|
503
530
|
TContext
|
|
504
531
|
>;
|
|
505
532
|
/** Handles `vcs.diff_file`. */
|
|
506
|
-
'vcs.diff_file'?: RpcMethodHandler<VcsDiffFileParams,
|
|
533
|
+
'vcs.diff_file'?: RpcMethodHandler<VcsDiffFileParams, VcsDiffFileResponse, TContext>;
|
|
507
534
|
/** Handles `vcs.diff_commit`. */
|
|
508
535
|
'vcs.diff_commit'?: RpcMethodHandler<VcsDiffCommitParams, string[], TContext>;
|
|
509
536
|
/** Handles `vcs.get_conflict_details`. */
|
|
@@ -526,6 +553,8 @@ export interface VcsDelegates<TContext = unknown> {
|
|
|
526
553
|
>;
|
|
527
554
|
/** Handles `vcs.stage_patch`. */
|
|
528
555
|
'vcs.stage_patch'?: RpcMethodHandler<VcsStagePatchParams, null, TContext>;
|
|
556
|
+
/** Handles `vcs.stage_selections` (structured hunk/line selections). */
|
|
557
|
+
'vcs.stage_selections'?: RpcMethodHandler<VcsStageSelectionsParams, null, TContext>;
|
|
529
558
|
/** Handles `vcs.stage_paths`. */
|
|
530
559
|
'vcs.stage_paths'?: RpcMethodHandler<VcsStagePathsParams, null, TContext>;
|
|
531
560
|
/** Handles `vcs.discard_paths`. */
|
package/test/build.test.js
CHANGED
|
@@ -16,6 +16,20 @@ const {
|
|
|
16
16
|
} = require("../lib/build");
|
|
17
17
|
const { cleanupTempDir, makeTempDir, writeJson, writeText } = require("./helpers");
|
|
18
18
|
|
|
19
|
+
function captureStderr(fn) {
|
|
20
|
+
const chunks = [];
|
|
21
|
+
const originalWrite = process.stderr.write;
|
|
22
|
+
process.stderr.write = (chunk) => {
|
|
23
|
+
chunks.push(String(chunk));
|
|
24
|
+
return true;
|
|
25
|
+
};
|
|
26
|
+
try {
|
|
27
|
+
return { result: fn(), stderr: chunks.join("") };
|
|
28
|
+
} finally {
|
|
29
|
+
process.stderr.write = originalWrite;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
test("renderGeneratedBootstrap creates ESM code", () => {
|
|
20
34
|
const output = require("../lib/build").renderGeneratedBootstrap("./plugin.js", true);
|
|
21
35
|
assert.match(output, /^#!/);
|
|
@@ -64,9 +78,11 @@ test("buildPluginAssets no-ops for theme-only plugins", () => {
|
|
|
64
78
|
name: "theme-only",
|
|
65
79
|
openvcs: { id: "theme-only" },
|
|
66
80
|
});
|
|
67
|
-
const manifest = buildPluginAssets({ pluginDir, verbose: false });
|
|
81
|
+
const { result: manifest, stderr } = captureStderr(() => buildPluginAssets({ pluginDir, verbose: false }));
|
|
68
82
|
|
|
69
83
|
assert.equal(manifest.pluginId, "theme-only");
|
|
84
|
+
assert.match(stderr, /openvcs build: reading plugin manifest/);
|
|
85
|
+
assert.match(stderr, /openvcs build: theme-only plugin theme-only; nothing to build/);
|
|
70
86
|
cleanupTempDir(root);
|
|
71
87
|
});
|
|
72
88
|
|
|
@@ -110,7 +126,7 @@ test("buildPluginAssets runs build:plugin and validates output", () => {
|
|
|
110
126
|
"const fs = require('node:fs');\nconst path = require('node:path');\nconst out = path.join(process.cwd(), 'bin', 'plugin.js');\nfs.mkdirSync(path.dirname(out), { recursive: true });\nfs.writeFileSync(out, 'export {};\\n', 'utf8');\n"
|
|
111
127
|
);
|
|
112
128
|
|
|
113
|
-
const manifest = buildPluginAssets({ pluginDir, verbose: false });
|
|
129
|
+
const { result: manifest, stderr } = captureStderr(() => buildPluginAssets({ pluginDir, verbose: false }));
|
|
114
130
|
|
|
115
131
|
assert.equal(manifest.pluginId, "builder");
|
|
116
132
|
assert.equal(fs.existsSync(path.join(pluginDir, "bin", "plugin.js")), true);
|
|
@@ -119,6 +135,42 @@ test("buildPluginAssets runs build:plugin and validates output", () => {
|
|
|
119
135
|
fs.readFileSync(path.join(pluginDir, "bin", "openvcs-plugin.js"), "utf8"),
|
|
120
136
|
/bootstrapPluginModule/
|
|
121
137
|
);
|
|
138
|
+
assert.match(stderr, /openvcs build: reading plugin manifest/);
|
|
139
|
+
assert.match(stderr, /openvcs build: running build:plugin/);
|
|
140
|
+
assert.match(stderr, /openvcs build: generating bootstrap openvcs-plugin.js/);
|
|
141
|
+
assert.match(stderr, /openvcs build: validating bootstrap openvcs-plugin.js/);
|
|
142
|
+
assert.match(stderr, /openvcs build: build complete for builder/);
|
|
143
|
+
cleanupTempDir(root);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("buildPluginAssets verbose mode adds detailed progress output", () => {
|
|
147
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
148
|
+
const pluginDir = path.join(root, "plugin");
|
|
149
|
+
|
|
150
|
+
writeJson(path.join(pluginDir, "package.json"), {
|
|
151
|
+
name: "verbose-builder",
|
|
152
|
+
private: true,
|
|
153
|
+
type: "module",
|
|
154
|
+
openvcs: {
|
|
155
|
+
id: "verbose-builder",
|
|
156
|
+
module: { exec: "openvcs-plugin.mjs" },
|
|
157
|
+
},
|
|
158
|
+
scripts: {
|
|
159
|
+
"build:plugin": "node ./scripts/build-plugin.cjs",
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
writeText(
|
|
163
|
+
path.join(pluginDir, "scripts", "build-plugin.cjs"),
|
|
164
|
+
"const fs = require('node:fs');\nconst path = require('node:path');\nconst out = path.join(process.cwd(), 'bin', 'plugin.js');\nfs.mkdirSync(path.dirname(out), { recursive: true });\nfs.writeFileSync(out, 'export {};\\n', 'utf8');\n"
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const { result: manifest, stderr } = captureStderr(() => buildPluginAssets({ pluginDir, verbose: true }));
|
|
168
|
+
|
|
169
|
+
assert.equal(manifest.pluginId, "verbose-builder");
|
|
170
|
+
assert.match(stderr, /openvcs build: reading manifest at .*package\.json/);
|
|
171
|
+
assert.match(stderr, /openvcs build: manifest loaded for verbose-builder \(module\.exec: openvcs-plugin\.mjs\)/);
|
|
172
|
+
assert.match(stderr, /Running command in .*: .* run build:plugin/);
|
|
173
|
+
assert.match(stderr, /openvcs build: writing bootstrap .*openvcs-plugin\.mjs -> .*plugin\.js \(esm\)/);
|
|
122
174
|
cleanupTempDir(root);
|
|
123
175
|
});
|
|
124
176
|
|
|
@@ -53,6 +53,34 @@ class DerivedBranchDelegates extends SharedBranchDelegates {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
class BinaryAwareDelegates extends VcsDelegateBase {
|
|
57
|
+
getStatusPayload() {
|
|
58
|
+
return {
|
|
59
|
+
files: [
|
|
60
|
+
{
|
|
61
|
+
path: 'img.png',
|
|
62
|
+
old_path: null,
|
|
63
|
+
status: 'M',
|
|
64
|
+
staged: false,
|
|
65
|
+
resolved_conflict: false,
|
|
66
|
+
hunks: [],
|
|
67
|
+
binary: true,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
ahead: 0,
|
|
71
|
+
behind: 0,
|
|
72
|
+
branch_on_remote: false,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
diffFile() {
|
|
77
|
+
return {
|
|
78
|
+
lines: ['Binary files a/img.png and b/img.png differ'],
|
|
79
|
+
binary: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
56
84
|
test('VcsDelegateBase maps overridden camelCase methods to rpc delegates', async () => {
|
|
57
85
|
const delegate = new ExampleVcsDelegates({ prefix: 'commit', calls: [] });
|
|
58
86
|
const delegates = delegate.toDelegates();
|
|
@@ -110,6 +138,44 @@ test('VcsDelegateBase keeps inherited overrides when building delegates', async
|
|
|
110
138
|
);
|
|
111
139
|
});
|
|
112
140
|
|
|
141
|
+
test('VcsDelegateBase preserves binary metadata in status and diff payloads', async () => {
|
|
142
|
+
const delegates = new BinaryAwareDelegates({}).toDelegates();
|
|
143
|
+
|
|
144
|
+
assert.deepEqual(
|
|
145
|
+
await delegates['vcs.get_status_payload'](
|
|
146
|
+
{ session_id: 'session-4' },
|
|
147
|
+
{ host: {}, method: 'vcs.get_status_payload', requestId: 11 },
|
|
148
|
+
),
|
|
149
|
+
{
|
|
150
|
+
files: [
|
|
151
|
+
{
|
|
152
|
+
path: 'img.png',
|
|
153
|
+
old_path: null,
|
|
154
|
+
status: 'M',
|
|
155
|
+
staged: false,
|
|
156
|
+
resolved_conflict: false,
|
|
157
|
+
hunks: [],
|
|
158
|
+
binary: true,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
ahead: 0,
|
|
162
|
+
behind: 0,
|
|
163
|
+
branch_on_remote: false,
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
assert.deepEqual(
|
|
168
|
+
await delegates['vcs.diff_file'](
|
|
169
|
+
{ session_id: 'session-4', path: 'img.png' },
|
|
170
|
+
{ host: {}, method: 'vcs.diff_file', requestId: 12 },
|
|
171
|
+
),
|
|
172
|
+
{
|
|
173
|
+
lines: ['Binary files a/img.png and b/img.png differ'],
|
|
174
|
+
binary: true,
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
113
179
|
test('VcsDelegateBase returns a fresh delegate map on each call', async () => {
|
|
114
180
|
const delegate = new ExampleVcsDelegates({ prefix: 'repeat', calls: [] });
|
|
115
181
|
const firstDelegates = delegate.toDelegates();
|