@openvcs/git-plugin 0.1.0-edge.20260422.23 → 0.1.0-edge.20260423.25

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.
@@ -3,6 +3,10 @@
3
3
  import { VcsDelegateBase, pluginError, } from '@openvcs/sdk/runtime';
4
4
  import { asNumber, asRecord, asString, asStringArray, asTrimmedString, buildCloneArgs, parseStatusOutput, } from './plugin-helpers.js';
5
5
  import { GitCommand } from './git.js';
6
+ /** Returns an optional boolean only when the input is already a boolean. */
7
+ function asOptionalBoolean(value) {
8
+ return typeof value === 'boolean' ? value : undefined;
9
+ }
6
10
  /** Reduces a file status string to the primary status code needed for discard routing. */
7
11
  function getPrimaryDiscardStatus(status) {
8
12
  const normalized = asTrimmedString(status);
@@ -237,8 +241,8 @@ export class GitVcsDelegates extends VcsDelegateBase {
237
241
  branch: asTrimmedString(query.rev) || undefined,
238
242
  skip: asNumber(query.skip, 0) || undefined,
239
243
  limit: asNumber(query.limit, 0),
240
- topo_order: query.topo_order ?? undefined,
241
- include_merges: query.include_merges ?? undefined,
244
+ topo_order: asOptionalBoolean(query.topo_order),
245
+ include_merges: asOptionalBoolean(query.include_merges),
242
246
  author_contains: asTrimmedString(query.author_contains) || undefined,
243
247
  since_utc: asTrimmedString(query.since_utc) || undefined,
244
248
  until_utc: asTrimmedString(query.until_utc) || undefined,
@@ -291,15 +295,29 @@ export class GitVcsDelegates extends VcsDelegateBase {
291
295
  }
292
296
  const status = git.runChecked(['status', '--porcelain=1', '-z', '-uall', '--', ...paths], 'git-discard-paths-failed');
293
297
  const discardPlan = planDiscardPaths(status.stdout);
298
+ let failure;
294
299
  if (discardPlan.unstageThenRemove.length > 0) {
295
- git.runChecked(['rm', '-f', '--cached', '--', ...discardPlan.unstageThenRemove], 'git-discard-paths-failed');
300
+ try {
301
+ git.runChecked(['rm', '-f', '--cached', '--', ...discardPlan.unstageThenRemove], 'git-discard-paths-failed');
302
+ }
303
+ catch (error) {
304
+ failure = error;
305
+ }
296
306
  }
297
- if (discardPlan.restore.length > 0) {
298
- git.runChecked(['restore', '--source=HEAD', '--staged', '--worktree', '--', ...discardPlan.restore], 'git-discard-paths-failed');
307
+ if (!failure && discardPlan.restore.length > 0) {
308
+ try {
309
+ git.runChecked(['restore', '--source=HEAD', '--staged', '--worktree', '--', ...discardPlan.restore], 'git-discard-paths-failed');
310
+ }
311
+ catch (error) {
312
+ failure = error;
313
+ }
299
314
  }
300
- if (discardPlan.clean.length > 0) {
315
+ if (!failure && discardPlan.clean.length > 0) {
301
316
  git.runChecked(['clean', '-f', '--', ...discardPlan.clean], 'git-discard-paths-failed');
302
317
  }
318
+ if (failure) {
319
+ throw failure;
320
+ }
303
321
  return null;
304
322
  }
305
323
  applyReversePatch(params, _context) {
package/bin/submodules.js CHANGED
@@ -18,7 +18,7 @@ function payloadString(payload, key) {
18
18
  return String(payload[key] ?? '').trim();
19
19
  }
20
20
  /** Builds one modal row for a submodule entry. */
21
- function buildSubmoduleRow(entry) {
21
+ export function buildSubmoduleRow(entry) {
22
22
  const metaBits = [entry.commit ? `commit ${entry.commit}` : '', entry.branch ? `branch ${entry.branch}` : '']
23
23
  .map((part) => String(part || '').trim())
24
24
  .filter(Boolean);
@@ -51,12 +51,44 @@ function buildSubmoduleRow(entry) {
51
51
  ],
52
52
  };
53
53
  }
54
+ /** Builds an error fallback modal with a descriptive message. */
55
+ function buildErrorModal(message) {
56
+ return new ModalBuilder('Error').text(message).text('Please try again or check the Git repository state.');
57
+ }
58
+ /** Opens a fallback modal for a submodule UI failure and preserves the original error on fallback failure. */
59
+ export async function handleSubmoduleModalError(prefix, error, openFallback = (message) => buildErrorModal(message).open()) {
60
+ const message = error instanceof Error ? error.message : String(error);
61
+ const fullMessage = `${prefix}: ${message}`;
62
+ console.error(`Git submodules: ${fullMessage}`);
63
+ try {
64
+ return await openFallback(fullMessage);
65
+ }
66
+ catch {
67
+ throw error;
68
+ }
69
+ }
54
70
  /** Builds and opens the submodule manager modal. */
55
71
  async function openSubmodulesModal() {
56
72
  const git = createGitCommand();
57
- const entries = git.listSubmodules();
73
+ let entries;
74
+ try {
75
+ entries = git.listSubmodules();
76
+ }
77
+ catch (error) {
78
+ return handleSubmoduleModalError('failed to list submodules', error);
79
+ }
58
80
  console.log('Git submodules: building modal', { count: entries.length });
59
- const modal = new ModalBuilder('Manage Submodules')
81
+ const modal = buildSubmodulesModal(entries);
82
+ try {
83
+ return await modal.open();
84
+ }
85
+ catch (error) {
86
+ return handleSubmoduleModalError('failed to open modal', error);
87
+ }
88
+ }
89
+ /** Builds the submodule manager modal with given entries. */
90
+ function buildSubmodulesModal(entries) {
91
+ return new ModalBuilder('Manage Submodules')
60
92
  .text('Review, add, update, sync, and remove submodules without leaving Git.')
61
93
  .text('Use Update Remote to follow each submodule branch configured in .gitmodules.')
62
94
  .separator()
@@ -109,7 +141,6 @@ async function openSubmodulesModal() {
109
141
  emptyText: 'This repository has no submodules yet.',
110
142
  items: entries.map((entry) => buildSubmoduleRow(entry)),
111
143
  });
112
- return modal.open();
113
144
  }
114
145
  /** Builds and opens the remove confirmation modal for one submodule. */
115
146
  async function openRemoveConfirmationModal(path, name) {
@@ -129,7 +160,12 @@ async function openRemoveConfirmationModal(path, name) {
129
160
  payload: { path: submodulePath, name: String(name || '').trim() || undefined },
130
161
  },
131
162
  ], { gap: '.5rem', align: 'centered', wrap: true });
132
- return modal.open();
163
+ try {
164
+ return await modal.open();
165
+ }
166
+ catch (error) {
167
+ return handleSubmoduleModalError('failed to open remove confirmation modal', error);
168
+ }
133
169
  }
134
170
  /** Removes one submodule and refreshes the toolkit modal. */
135
171
  async function removeSubmodule(payload) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openvcs/git-plugin",
3
- "version": "0.1.0-edge.20260422.23",
3
+ "version": "0.1.0-edge.20260423.25",
4
4
  "description": "OpenVCS Git plugin - Node.js runtime",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "homepage": "https://github.com/Open-VCS/OpenVCS-Plugin-Git",
@@ -18,7 +18,7 @@
18
18
  "openvcs": {
19
19
  "id": "openvcs.git",
20
20
  "name": "Git",
21
- "version": "0.1.0-edge.20260422.23",
21
+ "version": "0.1.0-edge.20260423.25",
22
22
  "author": "OpenVCS Contributors",
23
23
  "description": "Git VCS backend plugin for OpenVCS",
24
24
  "default_enabled": true,
@@ -30,6 +30,11 @@ export interface DiscardPathPlan {
30
30
  clean: string[];
31
31
  }
32
32
 
33
+ /** Returns an optional boolean only when the input is already a boolean. */
34
+ function asOptionalBoolean(value: unknown): boolean | undefined {
35
+ return typeof value === 'boolean' ? value : undefined;
36
+ }
37
+
33
38
  /** Reduces a file status string to the primary status code needed for discard routing. */
34
39
  function getPrimaryDiscardStatus(status: string): string {
35
40
  const normalized = asTrimmedString(status);
@@ -396,8 +401,8 @@ export class GitVcsDelegates extends VcsDelegateBase<GitRuntimeDependencies> {
396
401
  branch: asTrimmedString(query.rev) || undefined,
397
402
  skip: asNumber(query.skip, 0) || undefined,
398
403
  limit: asNumber(query.limit, 0),
399
- topo_order: query.topo_order as boolean ?? undefined,
400
- include_merges: query.include_merges as boolean ?? undefined,
404
+ topo_order: asOptionalBoolean(query.topo_order),
405
+ include_merges: asOptionalBoolean(query.include_merges),
401
406
  author_contains: asTrimmedString(query.author_contains) || undefined,
402
407
  since_utc: asTrimmedString(query.since_utc) || undefined,
403
408
  until_utc: asTrimmedString(query.until_utc) || undefined,
@@ -490,23 +495,38 @@ export class GitVcsDelegates extends VcsDelegateBase<GitRuntimeDependencies> {
490
495
  );
491
496
  const discardPlan = planDiscardPaths(status.stdout);
492
497
 
498
+ let failure: unknown;
499
+
493
500
  if (discardPlan.unstageThenRemove.length > 0) {
494
- git.runChecked(
495
- ['rm', '-f', '--cached', '--', ...discardPlan.unstageThenRemove],
496
- 'git-discard-paths-failed',
497
- );
501
+ try {
502
+ git.runChecked(
503
+ ['rm', '-f', '--cached', '--', ...discardPlan.unstageThenRemove],
504
+ 'git-discard-paths-failed',
505
+ );
506
+ } catch (error) {
507
+ failure = error;
508
+ }
498
509
  }
499
510
 
500
- if (discardPlan.restore.length > 0) {
501
- git.runChecked(
502
- ['restore', '--source=HEAD', '--staged', '--worktree', '--', ...discardPlan.restore],
503
- 'git-discard-paths-failed',
504
- );
511
+ if (!failure && discardPlan.restore.length > 0) {
512
+ try {
513
+ git.runChecked(
514
+ ['restore', '--source=HEAD', '--staged', '--worktree', '--', ...discardPlan.restore],
515
+ 'git-discard-paths-failed',
516
+ );
517
+ } catch (error) {
518
+ failure = error;
519
+ }
505
520
  }
506
521
 
507
- if (discardPlan.clean.length > 0) {
522
+ if (!failure && discardPlan.clean.length > 0) {
508
523
  git.runChecked(['clean', '-f', '--', ...discardPlan.clean], 'git-discard-paths-failed');
509
524
  }
525
+
526
+ if (failure) {
527
+ throw failure;
528
+ }
529
+
510
530
  return null;
511
531
  }
512
532
 
package/src/submodules.ts CHANGED
@@ -26,7 +26,7 @@ function payloadString(payload: ModalActionPayload, key: string): string {
26
26
  }
27
27
 
28
28
  /** Builds one modal row for a submodule entry. */
29
- function buildSubmoduleRow(entry: SubmoduleEntry) {
29
+ export function buildSubmoduleRow(entry: SubmoduleEntry) {
30
30
  const metaBits = [entry.commit ? `commit ${entry.commit}` : '', entry.branch ? `branch ${entry.branch}` : '']
31
31
  .map((part) => String(part || '').trim())
32
32
  .filter(Boolean);
@@ -62,13 +62,52 @@ function buildSubmoduleRow(entry: SubmoduleEntry) {
62
62
  };
63
63
  }
64
64
 
65
+ /** Builds an error fallback modal with a descriptive message. */
66
+ function buildErrorModal(message: string): ModalBuilder {
67
+ return new ModalBuilder('Error').text(message).text('Please try again or check the Git repository state.');
68
+ }
69
+
70
+ /** Opens a fallback modal for a submodule UI failure and preserves the original error on fallback failure. */
71
+ export async function handleSubmoduleModalError(
72
+ prefix: string,
73
+ error: unknown,
74
+ openFallback: (message: string) => Promise<unknown> = (message) => buildErrorModal(message).open(),
75
+ ): Promise<unknown> {
76
+ const message = error instanceof Error ? error.message : String(error);
77
+ const fullMessage = `${prefix}: ${message}`;
78
+ console.error(`Git submodules: ${fullMessage}`);
79
+
80
+ try {
81
+ return await openFallback(fullMessage);
82
+ } catch {
83
+ throw error;
84
+ }
85
+ }
86
+
65
87
  /** Builds and opens the submodule manager modal. */
66
88
  async function openSubmodulesModal(): Promise<unknown> {
67
89
  const git = createGitCommand();
68
- const entries = git.listSubmodules();
90
+
91
+ let entries: SubmoduleEntry[];
92
+ try {
93
+ entries = git.listSubmodules();
94
+ } catch (error) {
95
+ return handleSubmoduleModalError('failed to list submodules', error);
96
+ }
97
+
69
98
  console.log('Git submodules: building modal', { count: entries.length });
70
99
 
71
- const modal = new ModalBuilder('Manage Submodules')
100
+ const modal = buildSubmodulesModal(entries);
101
+ try {
102
+ return await modal.open();
103
+ } catch (error) {
104
+ return handleSubmoduleModalError('failed to open modal', error);
105
+ }
106
+ }
107
+
108
+ /** Builds the submodule manager modal with given entries. */
109
+ function buildSubmodulesModal(entries: SubmoduleEntry[]): ModalBuilder {
110
+ return new ModalBuilder('Manage Submodules')
72
111
  .text('Review, add, update, sync, and remove submodules without leaving Git.')
73
112
  .text('Use Update Remote to follow each submodule branch configured in .gitmodules.')
74
113
  .separator()
@@ -130,14 +169,13 @@ async function openSubmodulesModal(): Promise<unknown> {
130
169
  emptyText: 'This repository has no submodules yet.',
131
170
  items: entries.map((entry) => buildSubmoduleRow(entry)),
132
171
  });
133
-
134
- return modal.open();
135
172
  }
136
173
 
137
174
  /** Builds and opens the remove confirmation modal for one submodule. */
138
175
  async function openRemoveConfirmationModal(path: string, name?: string): Promise<unknown> {
139
176
  const submodulePath = String(path || '').trim();
140
177
  if (!submodulePath) return;
178
+
141
179
  console.log('Git submodules: building remove confirmation', { path: submodulePath, name });
142
180
 
143
181
  const modal = new ModalBuilder('Confirm Submodule Removal')
@@ -156,7 +194,11 @@ async function openRemoveConfirmationModal(path: string, name?: string): Promise
156
194
  { gap: '.5rem', align: 'centered', wrap: true },
157
195
  );
158
196
 
159
- return modal.open();
197
+ try {
198
+ return await modal.open();
199
+ } catch (error) {
200
+ return handleSubmoduleModalError('failed to open remove confirmation modal', error);
201
+ }
160
202
  }
161
203
 
162
204
  /** Removes one submodule and refreshes the toolkit modal. */