@openvcs/sdk 0.2.5 → 0.2.7

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,165 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import type * as VcsTypes from '../types';
5
+
6
+ import type { PluginRuntimeContext } from './contracts';
7
+
8
+ /** Stores the typed class-method signatures supported by `VcsDelegateBase`. */
9
+ export type VcsDelegateBindings<TContext> = {
10
+ getCaps: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.get_caps']>;
11
+ open: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.open']>;
12
+ close: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.close']>;
13
+ cloneRepo: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.clone_repo']>;
14
+ getWorkdir: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.get_workdir']>;
15
+ getCurrentBranch: NonNullable<
16
+ VcsTypes.VcsDelegates<TContext>['vcs.get_current_branch']
17
+ >;
18
+ listBranches: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.list_branches']>;
19
+ listLocalBranches: NonNullable<
20
+ VcsTypes.VcsDelegates<TContext>['vcs.list_local_branches']
21
+ >;
22
+ createBranch: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.create_branch']>;
23
+ checkoutBranch: NonNullable<
24
+ VcsTypes.VcsDelegates<TContext>['vcs.checkout_branch']
25
+ >;
26
+ ensureRemote: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.ensure_remote']>;
27
+ listRemotes: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.list_remotes']>;
28
+ removeRemote: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.remove_remote']>;
29
+ fetch: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.fetch']>;
30
+ fetchWithOptions: NonNullable<
31
+ VcsTypes.VcsDelegates<TContext>['vcs.fetch_with_options']
32
+ >;
33
+ push: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.push']>;
34
+ pullFfOnly: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.pull_ff_only']>;
35
+ commit: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.commit']>;
36
+ commitIndex: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.commit_index']>;
37
+ getStatusSummary: NonNullable<
38
+ VcsTypes.VcsDelegates<TContext>['vcs.get_status_summary']
39
+ >;
40
+ getStatusPayload: NonNullable<
41
+ VcsTypes.VcsDelegates<TContext>['vcs.get_status_payload']
42
+ >;
43
+ listCommits: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.list_commits']>;
44
+ diffFile: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.diff_file']>;
45
+ diffCommit: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.diff_commit']>;
46
+ getConflictDetails: NonNullable<
47
+ VcsTypes.VcsDelegates<TContext>['vcs.get_conflict_details']
48
+ >;
49
+ checkoutConflictSide: NonNullable<
50
+ VcsTypes.VcsDelegates<TContext>['vcs.checkout_conflict_side']
51
+ >;
52
+ writeMergeResult: NonNullable<
53
+ VcsTypes.VcsDelegates<TContext>['vcs.write_merge_result']
54
+ >;
55
+ stagePatch: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.stage_patch']>;
56
+ discardPaths: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.discard_paths']>;
57
+ applyReversePatch: NonNullable<
58
+ VcsTypes.VcsDelegates<TContext>['vcs.apply_reverse_patch']
59
+ >;
60
+ deleteBranch: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.delete_branch']>;
61
+ renameBranch: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.rename_branch']>;
62
+ mergeIntoCurrent: NonNullable<
63
+ VcsTypes.VcsDelegates<TContext>['vcs.merge_into_current']
64
+ >;
65
+ mergeAbort: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.merge_abort']>;
66
+ mergeContinue: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.merge_continue']>;
67
+ isMergeInProgress: NonNullable<
68
+ VcsTypes.VcsDelegates<TContext>['vcs.is_merge_in_progress']
69
+ >;
70
+ setBranchUpstream: NonNullable<
71
+ VcsTypes.VcsDelegates<TContext>['vcs.set_branch_upstream']
72
+ >;
73
+ getBranchUpstream: NonNullable<
74
+ VcsTypes.VcsDelegates<TContext>['vcs.get_branch_upstream']
75
+ >;
76
+ hardResetHead: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.hard_reset_head']>;
77
+ resetSoftTo: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.reset_soft_to']>;
78
+ getIdentity: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.get_identity']>;
79
+ setIdentityLocal: NonNullable<
80
+ VcsTypes.VcsDelegates<TContext>['vcs.set_identity_local']
81
+ >;
82
+ listStashes: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.list_stashes']>;
83
+ stashPush: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.stash_push']>;
84
+ stashApply: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.stash_apply']>;
85
+ stashPop: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.stash_pop']>;
86
+ stashDrop: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.stash_drop']>;
87
+ stashShow: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.stash_show']>;
88
+ cherryPick: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.cherry_pick']>;
89
+ revertCommit: NonNullable<VcsTypes.VcsDelegates<TContext>['vcs.revert_commit']>;
90
+ };
91
+
92
+ /** Enumerates the class-friendly method names recognized by `VcsDelegateBase`. */
93
+ export type VcsDelegateMethodName = keyof VcsDelegateBindings<PluginRuntimeContext>;
94
+
95
+ /** Maps one delegate class method to the SDK runtime RPC key it implements. */
96
+ export const VCS_DELEGATE_METHOD_MAPPINGS = {
97
+ getCaps: 'vcs.get_caps',
98
+ open: 'vcs.open',
99
+ close: 'vcs.close',
100
+ cloneRepo: 'vcs.clone_repo',
101
+ getWorkdir: 'vcs.get_workdir',
102
+ getCurrentBranch: 'vcs.get_current_branch',
103
+ listBranches: 'vcs.list_branches',
104
+ listLocalBranches: 'vcs.list_local_branches',
105
+ createBranch: 'vcs.create_branch',
106
+ checkoutBranch: 'vcs.checkout_branch',
107
+ ensureRemote: 'vcs.ensure_remote',
108
+ listRemotes: 'vcs.list_remotes',
109
+ removeRemote: 'vcs.remove_remote',
110
+ fetch: 'vcs.fetch',
111
+ fetchWithOptions: 'vcs.fetch_with_options',
112
+ push: 'vcs.push',
113
+ pullFfOnly: 'vcs.pull_ff_only',
114
+ commit: 'vcs.commit',
115
+ commitIndex: 'vcs.commit_index',
116
+ getStatusSummary: 'vcs.get_status_summary',
117
+ getStatusPayload: 'vcs.get_status_payload',
118
+ listCommits: 'vcs.list_commits',
119
+ diffFile: 'vcs.diff_file',
120
+ diffCommit: 'vcs.diff_commit',
121
+ getConflictDetails: 'vcs.get_conflict_details',
122
+ checkoutConflictSide: 'vcs.checkout_conflict_side',
123
+ writeMergeResult: 'vcs.write_merge_result',
124
+ stagePatch: 'vcs.stage_patch',
125
+ discardPaths: 'vcs.discard_paths',
126
+ applyReversePatch: 'vcs.apply_reverse_patch',
127
+ deleteBranch: 'vcs.delete_branch',
128
+ renameBranch: 'vcs.rename_branch',
129
+ mergeIntoCurrent: 'vcs.merge_into_current',
130
+ mergeAbort: 'vcs.merge_abort',
131
+ mergeContinue: 'vcs.merge_continue',
132
+ isMergeInProgress: 'vcs.is_merge_in_progress',
133
+ setBranchUpstream: 'vcs.set_branch_upstream',
134
+ getBranchUpstream: 'vcs.get_branch_upstream',
135
+ hardResetHead: 'vcs.hard_reset_head',
136
+ resetSoftTo: 'vcs.reset_soft_to',
137
+ getIdentity: 'vcs.get_identity',
138
+ setIdentityLocal: 'vcs.set_identity_local',
139
+ listStashes: 'vcs.list_stashes',
140
+ stashPush: 'vcs.stash_push',
141
+ stashApply: 'vcs.stash_apply',
142
+ stashPop: 'vcs.stash_pop',
143
+ stashDrop: 'vcs.stash_drop',
144
+ stashShow: 'vcs.stash_show',
145
+ cherryPick: 'vcs.cherry_pick',
146
+ revertCommit: 'vcs.revert_commit',
147
+ } as const satisfies Record<
148
+ VcsDelegateMethodName,
149
+ keyof VcsTypes.VcsDelegates<PluginRuntimeContext>
150
+ >;
151
+
152
+ /** Resolves the RPC method implemented by one class-friendly delegate method. */
153
+ export type VcsDelegateRpcMethodName<TMethodName extends VcsDelegateMethodName> =
154
+ (typeof VCS_DELEGATE_METHOD_MAPPINGS)[TMethodName];
155
+
156
+ /** Describes the callable prototype surface required by `toDelegates()`. */
157
+ export type VcsDelegatePrototype<TContext> = {
158
+ [TMethodName in keyof VcsDelegateBindings<TContext>]: VcsDelegateBindings<TContext>[TMethodName];
159
+ };
160
+
161
+ /** Stores the exact `vcs.*` delegate object shape produced from class methods. */
162
+ export type VcsDelegateAssignments<TContext> = {
163
+ [TMethodName in VcsDelegateMethodName as VcsDelegateRpcMethodName<TMethodName>]?:
164
+ VcsDelegateBindings<TContext>[TMethodName];
165
+ };
@@ -238,6 +238,8 @@ export interface CommitEntry {
238
238
  author: string;
239
239
  /** Stores the formatted metadata string. */
240
240
  meta: string;
241
+ /** Stores the first parent commit id when available. */
242
+ parent_oid?: string;
241
243
  }
242
244
 
243
245
  /** Describes params for `vcs.diff_file`. */
@@ -352,6 +354,18 @@ export interface VcsResetSoftToParams extends VcsSessionParams {
352
354
  rev: string;
353
355
  }
354
356
 
357
+ /** Describes params for continuing a merge in progress. */
358
+ export interface VcsMergeContinueParams extends VcsSessionParams {
359
+ /** Stores an optional commit message override. */
360
+ message?: string;
361
+ }
362
+
363
+ /** Describes params for hard resetting HEAD to a given ref. */
364
+ export interface VcsHardResetHeadParams extends VcsSessionParams {
365
+ /** Stores the ref to reset HEAD to; defaults to HEAD. */
366
+ ref?: string;
367
+ }
368
+
355
369
  /** Describes one configured author identity. */
356
370
  export interface VcsIdentity {
357
371
  /** Stores the configured author name. */
@@ -525,7 +539,7 @@ export interface VcsDelegates<TContext = unknown> {
525
539
  /** Handles `vcs.merge_abort`. */
526
540
  'vcs.merge_abort'?: RpcMethodHandler<VcsSessionParams, null, TContext>;
527
541
  /** Handles `vcs.merge_continue`. */
528
- 'vcs.merge_continue'?: RpcMethodHandler<VcsSessionParams, null, TContext>;
542
+ 'vcs.merge_continue'?: RpcMethodHandler<VcsMergeContinueParams, null, TContext>;
529
543
  /** Handles `vcs.is_merge_in_progress`. */
530
544
  'vcs.is_merge_in_progress'?: RpcMethodHandler<
531
545
  VcsSessionParams,
@@ -545,7 +559,7 @@ export interface VcsDelegates<TContext = unknown> {
545
559
  TContext
546
560
  >;
547
561
  /** Handles `vcs.hard_reset_head`. */
548
- 'vcs.hard_reset_head'?: RpcMethodHandler<VcsSessionParams, null, TContext>;
562
+ 'vcs.hard_reset_head'?: RpcMethodHandler<VcsHardResetHeadParams, null, TContext>;
549
563
  /** Handles `vcs.reset_soft_to`. */
550
564
  'vcs.reset_soft_to'?: RpcMethodHandler<VcsResetSoftToParams, null, TContext>;
551
565
  /** Handles `vcs.get_identity`. */
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "rootDir": ".."
6
+ },
7
+ "include": ["../src/**/*.ts", "./vcs-delegate-base.types.ts", "./vcs-delegate-base.test.ts"]
8
+ }
@@ -0,0 +1,169 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ const assert = require('node:assert/strict');
5
+ const test = require('node:test');
6
+
7
+ const { VcsDelegateBase } = require('../lib/runtime');
8
+
9
+ /**
10
+ * Compile-time override safety lives in `vcs-delegate-base.types.ts`, which
11
+ * verifies that generic parameters are explicit and incompatible signatures
12
+ * (wrong return type or params) fail the TypeScript compiler. This file
13
+ * tests runtime behavior using the plain Node test runner.
14
+ * @typedef {{ message: string, method: string }} CommitCall */
15
+
16
+ class ExampleVcsDelegates extends VcsDelegateBase {
17
+ /** @param {{ prefix: string, calls: CommitCall[] }} deps */
18
+ constructor(deps) {
19
+ super(deps);
20
+ }
21
+
22
+ getCaps() {
23
+ return {
24
+ commits: true,
25
+ branches: true,
26
+ tags: false,
27
+ staging: true,
28
+ push_pull: false,
29
+ fast_forward: true,
30
+ };
31
+ }
32
+
33
+ async commit(params, context) {
34
+ this.deps.calls.push({ message: params.message, method: context.method });
35
+ return `${this.deps.prefix}:${params.message}`;
36
+ }
37
+ }
38
+
39
+ class SharedBranchDelegates extends VcsDelegateBase {
40
+ /** @param {{}} deps */
41
+ constructor(deps) {
42
+ super(deps);
43
+ }
44
+
45
+ getCurrentBranch(params) {
46
+ return `branch:${params.session_id}`;
47
+ }
48
+ }
49
+
50
+ class DerivedBranchDelegates extends SharedBranchDelegates {
51
+ listBranches() {
52
+ return [];
53
+ }
54
+ }
55
+
56
+ test('VcsDelegateBase maps overridden camelCase methods to rpc delegates', async () => {
57
+ const delegate = new ExampleVcsDelegates({ prefix: 'commit', calls: [] });
58
+ const delegates = delegate.toDelegates();
59
+
60
+ assert.deepEqual(Object.keys(delegates).sort(), ['vcs.commit', 'vcs.get_caps']);
61
+ assert.equal(typeof delegates['vcs.get_caps'], 'function');
62
+ assert.equal(typeof delegates['vcs.commit'], 'function');
63
+
64
+ assert.deepEqual(await delegates['vcs.get_caps']({}, {}), {
65
+ commits: true,
66
+ branches: true,
67
+ tags: false,
68
+ staging: true,
69
+ push_pull: false,
70
+ fast_forward: true,
71
+ });
72
+
73
+ assert.equal(
74
+ await delegates['vcs.commit'](
75
+ {
76
+ session_id: 'session-1',
77
+ name: 'OpenVCS',
78
+ email: 'team@example.com',
79
+ message: 'ship it',
80
+ },
81
+ { host: {}, method: 'vcs.commit', requestId: 7 },
82
+ ),
83
+ 'commit:ship it',
84
+ );
85
+ assert.deepEqual(delegate.deps.calls, [
86
+ { message: 'ship it', method: 'vcs.commit' },
87
+ ]);
88
+ });
89
+
90
+ test('VcsDelegateBase keeps inherited overrides when building delegates', async () => {
91
+ const delegates = new DerivedBranchDelegates({}).toDelegates();
92
+
93
+ assert.deepEqual(Object.keys(delegates).sort(), [
94
+ 'vcs.get_current_branch',
95
+ 'vcs.list_branches',
96
+ ]);
97
+ assert.equal(
98
+ await delegates['vcs.get_current_branch'](
99
+ { session_id: 'session-2' },
100
+ { host: {}, method: 'vcs.get_current_branch', requestId: 8 },
101
+ ),
102
+ 'branch:session-2',
103
+ );
104
+ assert.deepEqual(
105
+ await delegates['vcs.list_branches'](
106
+ { session_id: 'session-2' },
107
+ { host: {}, method: 'vcs.list_branches', requestId: 9 },
108
+ ),
109
+ [],
110
+ );
111
+ });
112
+
113
+ test('VcsDelegateBase returns a fresh delegate map on each call', async () => {
114
+ const delegate = new ExampleVcsDelegates({ prefix: 'repeat', calls: [] });
115
+ const firstDelegates = delegate.toDelegates();
116
+ const secondDelegates = delegate.toDelegates();
117
+
118
+ assert.notStrictEqual(firstDelegates, secondDelegates);
119
+ assert.notStrictEqual(
120
+ firstDelegates['vcs.commit'],
121
+ secondDelegates['vcs.commit'],
122
+ );
123
+ assert.equal(
124
+ await secondDelegates['vcs.commit'](
125
+ {
126
+ session_id: 'session-3',
127
+ name: 'OpenVCS',
128
+ email: 'team@example.com',
129
+ message: 'again',
130
+ },
131
+ { host: {}, method: 'vcs.commit', requestId: 10 },
132
+ ),
133
+ 'repeat:again',
134
+ );
135
+ });
136
+
137
+ test('VcsDelegateBase throws with the exact error message for base stubs', () => {
138
+ const delegate = new ExampleVcsDelegates({ prefix: 'x', calls: [] });
139
+
140
+ // Each base stub throws with a message that includes the method name.
141
+ // Test a representative subset; all stubs share the same formatter.
142
+ const expectedMessage = "VCS delegate method 'cloneRepo' must be overridden before registration";
143
+ let thrown;
144
+ try {
145
+ delegate.cloneRepo({ url: 'x', dest: 'y' }, {});
146
+ } catch (e) {
147
+ thrown = e;
148
+ }
149
+ assert.ok(thrown instanceof Error, 'should throw an Error');
150
+ assert.strictEqual(thrown.message, expectedMessage);
151
+
152
+ // Verify a second stub throws with its own method name in the message.
153
+ const expectedMessage2 = "VCS delegate method 'stashPush' must be overridden before registration";
154
+ let thrown2;
155
+ try {
156
+ delegate.stashPush({ session_id: 's' }, {});
157
+ } catch (e) {
158
+ thrown2 = e;
159
+ }
160
+ assert.ok(thrown2 instanceof Error);
161
+ assert.strictEqual(thrown2.message, expectedMessage2);
162
+ });
163
+
164
+ test('VcsDelegateBase accepts an empty deps object without errors', () => {
165
+ assert.doesNotThrow(() => new ExampleVcsDelegates({ prefix: '', calls: [] }));
166
+ assert.doesNotThrow(() => new SharedBranchDelegates({}));
167
+ const delegates = new SharedBranchDelegates({}).toDelegates();
168
+ assert.deepEqual(Object.keys(delegates), ['vcs.get_current_branch']);
169
+ });
@@ -0,0 +1,44 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import type { RequestParams, VcsCapabilities } from '../src/lib/types';
5
+
6
+ import {
7
+ VcsDelegateBase,
8
+ type PluginRuntimeContext,
9
+ } from '../src/lib/runtime';
10
+
11
+ type CommitCall = {
12
+ message: string;
13
+ method: string;
14
+ };
15
+
16
+ class TypedExampleVcsDelegates extends VcsDelegateBase<{
17
+ prefix: string;
18
+ calls: CommitCall[];
19
+ }> {
20
+ override getCaps(
21
+ _params: RequestParams,
22
+ _context: PluginRuntimeContext,
23
+ ): VcsCapabilities {
24
+ return {
25
+ commits: true,
26
+ branches: true,
27
+ tags: false,
28
+ staging: true,
29
+ push_pull: false,
30
+ fast_forward: true,
31
+ };
32
+ }
33
+ }
34
+
35
+ new TypedExampleVcsDelegates({ prefix: 'commit', calls: [] }).toDelegates();
36
+
37
+ class InvalidVcsDelegates extends VcsDelegateBase<{}> {
38
+ // @ts-expect-error `getCaps` must return `VcsCapabilities`.
39
+ override getCaps(): string {
40
+ return 'invalid';
41
+ }
42
+ }
43
+
44
+ new InvalidVcsDelegates({});