@openvcs/sdk 0.4.0 → 0.4.1-beta.128

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 CHANGED
@@ -5,6 +5,8 @@
5
5
 
6
6
  OpenVCS SDK for npm-based plugin development.
7
7
 
8
+ Requires Node.js 20 or newer.
9
+
8
10
  Install this package in plugin projects, scaffold a starter plugin, and build
9
11
  plugin runtime assets. The SDK also exports a Node-only JSON-RPC runtime
10
12
  layer and shared protocol/types so plugins do not have to hand-roll stdio
@@ -41,6 +43,16 @@ Run tests (builds first):
41
43
  npm test
42
44
  ```
43
45
 
46
+ Generate a coverage report:
47
+
48
+ ```bash
49
+ npm run coverage
50
+ ```
51
+
52
+ This runs the full test flow, writes c8 output to `coverage/`, and fails if
53
+ coverage drops below 95% lines, 95% functions, 85% branches, or 95%
54
+ statements.
55
+
44
56
  Run the local CLI through npm scripts:
45
57
 
46
58
  ```bash
@@ -162,6 +174,9 @@ compiled plugin author module at `bin/plugin.js`, and then generates the SDK-own
162
174
  `bin/<module.exec>` bootstrap that imports `./plugin.js`, applies `PluginDefinition`,
163
175
  invokes `OnPluginStart()`, and starts the runtime.
164
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
+
165
180
  Theme-only plugins can also run `npm run build`; the command exits successfully
166
181
  without producing `bin/` output.
167
182
 
@@ -196,18 +211,18 @@ Dependency behavior:
196
211
 
197
212
  ## CLI usage
198
213
 
199
- Build a plugin manually:
214
+ Build plugin assets from a plugin project:
200
215
 
201
216
  ```bash
202
- npx openvcs build --plugin-dir /path/to/plugin
217
+ npm run build
203
218
  ```
204
219
 
205
- Show command help:
220
+ Show command help through npm script:
206
221
 
207
222
  ```bash
208
- npx openvcs --help
209
- npx openvcs build --help
210
- npx openvcs init --help
223
+ npm run openvcs -- --help
224
+ npm run openvcs -- build --help
225
+ npm run openvcs -- init --help
211
226
  ```
212
227
 
213
228
  ## Publishing Note
package/lib/build.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { npmExecutable } from "./npm-runner";
1
2
  /** CLI arguments for `openvcs build`. */
2
3
  export interface BuildArgs {
3
4
  pluginDir: string;
@@ -10,26 +11,22 @@ export interface ManifestInfo {
10
11
  entry: string | undefined;
11
12
  manifestPath: string;
12
13
  }
13
- /** Returns the npm executable name for the current platform. */
14
- export declare function npmExecutable(): string;
15
- /** Returns whether a command must be launched via the Windows shell. */
16
- export declare function shouldUseWindowsShell(program: string): boolean;
17
14
  /** Formats help text for the build command. */
18
15
  export declare function buildUsage(commandName?: string): string;
19
16
  /** Parses `openvcs build` arguments. */
20
17
  export declare function parseBuildArgs(args: string[]): BuildArgs;
21
18
  /** Reads and validates the plugin manifest. */
22
- export declare function readManifest(pluginDir: string): ManifestInfo;
19
+ export declare function readManifest(pluginDir: string, verbose?: boolean): ManifestInfo;
23
20
  /** Verifies that a declared module entry resolves to a real file under `bin/`. */
24
- export declare function validateDeclaredModuleExec(pluginDir: string, moduleExec: string | undefined): void;
21
+ export declare function validateDeclaredModuleExec(pluginDir: string, moduleExec: string | undefined, verbose?: boolean): void;
25
22
  /** Returns the compiled plugin module path imported by the generated bootstrap. */
26
23
  export declare function authoredPluginModulePath(pluginDir: string): string;
27
24
  /** Ensures the plugin's authored module and generated bootstrap paths are compatible. */
28
- export declare function validateGeneratedBootstrapTargets(pluginDir: string, moduleExec: string | undefined): void;
25
+ export declare function validateGeneratedBootstrapTargets(pluginDir: string, moduleExec: string | undefined, verbose?: boolean): void;
29
26
  /** Renders the generated Node bootstrap that owns runtime startup. */
30
27
  export declare function renderGeneratedBootstrap(pluginModuleImportPath: string, isEsm: boolean): string;
31
28
  /** Writes the generated SDK-owned module entrypoint under `bin/<module.exec>`. */
32
- export declare function generateModuleBootstrap(pluginDir: string, moduleExec: string | undefined): void;
29
+ export declare function generateModuleBootstrap(pluginDir: string, moduleExec: string | undefined, verbose?: boolean): void;
33
30
  /** Returns whether the plugin repository has a `package.json`. */
34
31
  export declare function hasPackageJson(pluginDir: string): boolean;
35
32
  /** Runs a command in the given directory with optional verbose logging. */
package/lib/build.js CHANGED
@@ -35,8 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  };
36
36
  })();
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
- exports.npmExecutable = npmExecutable;
39
- exports.shouldUseWindowsShell = shouldUseWindowsShell;
38
+ exports.npmExecutable = void 0;
40
39
  exports.buildUsage = buildUsage;
41
40
  exports.parseBuildArgs = parseBuildArgs;
42
41
  exports.readManifest = readManifest;
@@ -52,19 +51,13 @@ const fs = __importStar(require("node:fs"));
52
51
  const path = __importStar(require("node:path"));
53
52
  const node_child_process_1 = require("node:child_process");
54
53
  const fs_utils_1 = require("./fs-utils");
55
- const AUTHORED_PLUGIN_MODULE_BASENAME = "plugin.js";
56
- /** Returns the npm executable name for the current platform. */
57
- function npmExecutable() {
58
- return "npm";
59
- }
60
- /** Returns whether a command must be launched via the Windows shell. */
61
- function shouldUseWindowsShell(program) {
62
- if (process.platform !== "win32") {
63
- return false;
64
- }
65
- const normalized = program.toLowerCase();
66
- return normalized === "npm" || normalized.endsWith(".cmd") || normalized.endsWith(".bat");
54
+ const npm_runner_1 = require("./npm-runner");
55
+ var npm_runner_2 = require("./npm-runner");
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`);
67
59
  }
60
+ const AUTHORED_PLUGIN_MODULE_BASENAME = "plugin.js";
68
61
  /** Formats help text for the build command. */
69
62
  function buildUsage(commandName = "openvcs") {
70
63
  return `${commandName} build [args]\n\n --plugin-dir <path> Plugin repository root (contains package.json with openvcs metadata)\n -V, --verbose Enable verbose output\n`;
@@ -100,11 +93,14 @@ function parseBuildArgs(args) {
100
93
  };
101
94
  }
102
95
  /** Reads and validates the plugin manifest. */
103
- function readManifest(pluginDir) {
96
+ function readManifest(pluginDir, verbose = false) {
104
97
  const manifestPath = path.join(pluginDir, "package.json");
105
98
  let manifestRaw;
106
99
  let manifestFd;
107
100
  let manifest;
101
+ if (verbose) {
102
+ writeBuildProgress(`reading manifest at ${manifestPath}`);
103
+ }
108
104
  try {
109
105
  manifestFd = fs.openSync(manifestPath, "r");
110
106
  const manifestStat = fs.fstatSync(manifestFd);
@@ -148,6 +144,9 @@ function readManifest(pluginDir) {
148
144
  const moduleExec = typeof moduleValue?.exec === "string" ? moduleValue.exec.trim() : undefined;
149
145
  const entryValue = openvcs.entry;
150
146
  const entry = typeof entryValue === "string" ? entryValue.trim() : undefined;
147
+ if (verbose) {
148
+ writeBuildProgress(`manifest loaded for ${pluginId}${moduleExec ? ` (module.exec: ${moduleExec})` : ""}`);
149
+ }
151
150
  return {
152
151
  pluginId,
153
152
  moduleExec,
@@ -156,11 +155,14 @@ function readManifest(pluginDir) {
156
155
  };
157
156
  }
158
157
  /** Verifies that a declared module entry resolves to a real file under `bin/`. */
159
- function validateDeclaredModuleExec(pluginDir, moduleExec) {
158
+ function validateDeclaredModuleExec(pluginDir, moduleExec, verbose = false) {
160
159
  if (!moduleExec) {
161
160
  return;
162
161
  }
163
162
  const targetPath = resolveDeclaredModuleExecPath(pluginDir, moduleExec);
163
+ if (verbose) {
164
+ writeBuildProgress(`checking declared bootstrap at ${targetPath}`);
165
+ }
164
166
  if (!fs.existsSync(targetPath) || !fs.lstatSync(targetPath).isFile()) {
165
167
  throw new Error(`module entrypoint not found at ${targetPath}`);
166
168
  }
@@ -187,10 +189,13 @@ function authoredPluginModulePath(pluginDir) {
187
189
  return path.resolve(pluginDir, "bin", AUTHORED_PLUGIN_MODULE_BASENAME);
188
190
  }
189
191
  /** Ensures the plugin's authored module and generated bootstrap paths are compatible. */
190
- function validateGeneratedBootstrapTargets(pluginDir, moduleExec) {
192
+ function validateGeneratedBootstrapTargets(pluginDir, moduleExec, verbose = false) {
191
193
  if (!moduleExec) {
192
194
  return;
193
195
  }
196
+ if (verbose) {
197
+ writeBuildProgress(`validating generated bootstrap target for ${moduleExec}`);
198
+ }
194
199
  resolveDeclaredModuleExecPath(pluginDir, moduleExec);
195
200
  const normalizedExec = moduleExec.trim().toLowerCase();
196
201
  if (normalizedExec === AUTHORED_PLUGIN_MODULE_BASENAME.toLowerCase()) {
@@ -221,16 +226,21 @@ function renderGeneratedBootstrap(pluginModuleImportPath, isEsm) {
221
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`;
222
227
  }
223
228
  /** Writes the generated SDK-owned module entrypoint under `bin/<module.exec>`. */
224
- function generateModuleBootstrap(pluginDir, moduleExec) {
229
+ function generateModuleBootstrap(pluginDir, moduleExec, verbose = false) {
225
230
  if (!moduleExec) {
226
- console.debug(`generateModuleBootstrap: no module.exec defined, skipping bootstrap generation for ${pluginDir}`);
231
+ if (verbose) {
232
+ writeBuildProgress(`no module.exec defined for ${pluginDir}; skipping bootstrap generation`);
233
+ }
227
234
  return;
228
235
  }
229
- validateGeneratedBootstrapTargets(pluginDir, moduleExec);
236
+ validateGeneratedBootstrapTargets(pluginDir, moduleExec, verbose);
230
237
  const execPath = resolveDeclaredModuleExecPath(pluginDir, moduleExec);
231
238
  const pluginModulePath = authoredPluginModulePath(pluginDir);
232
239
  const pluginModuleImportPath = relativeBinImport(execPath, pluginModulePath);
233
240
  const isEsm = detectEsmMode(pluginDir, moduleExec);
241
+ if (verbose) {
242
+ writeBuildProgress(`writing bootstrap ${execPath} -> ${pluginModuleImportPath}${isEsm ? " (esm)" : " (cjs)"}`);
243
+ }
234
244
  fs.mkdirSync(path.dirname(execPath), { recursive: true });
235
245
  fs.writeFileSync(execPath, renderGeneratedBootstrap(pluginModuleImportPath, isEsm), "utf8");
236
246
  }
@@ -262,8 +272,8 @@ function runCommand(program, args, cwd, verbose) {
262
272
  }
263
273
  const result = (0, node_child_process_1.spawnSync)(program, args, {
264
274
  cwd,
265
- shell: shouldUseWindowsShell(program),
266
275
  stdio: ["ignore", verbose ? "inherit" : "ignore", "inherit"],
276
+ windowsHide: true,
267
277
  });
268
278
  if (result.error) {
269
279
  throw new Error(`failed to spawn '${program}' in ${cwd}: ${result.error.message}`);
@@ -295,10 +305,13 @@ function readPackageScripts(pluginDir) {
295
305
  }
296
306
  /** Builds a plugin's runtime assets when it declares a code module. */
297
307
  function buildPluginAssets(parsedArgs) {
298
- const manifest = readManifest(parsedArgs.pluginDir);
308
+ writeBuildProgress(`reading plugin manifest in ${parsedArgs.pluginDir}`);
309
+ const manifest = readManifest(parsedArgs.pluginDir, parsedArgs.verbose);
299
310
  if (!manifest.moduleExec) {
311
+ writeBuildProgress(`theme-only plugin ${manifest.pluginId}; nothing to build`);
300
312
  return manifest;
301
313
  }
314
+ writeBuildProgress(`building plugin ${manifest.pluginId}`);
302
315
  if (!hasPackageJson(parsedArgs.pluginDir)) {
303
316
  throw new Error(`code plugins must include package.json: ${path.join(parsedArgs.pluginDir, "package.json")}`);
304
317
  }
@@ -307,8 +320,12 @@ function buildPluginAssets(parsedArgs) {
307
320
  if (typeof buildScript !== "string" || buildScript.trim() === "") {
308
321
  throw new Error(`code plugins must define scripts[\"build:plugin\"] in ${path.join(parsedArgs.pluginDir, "package.json")}`);
309
322
  }
310
- runCommand(npmExecutable(), ["run", "build:plugin"], parsedArgs.pluginDir, parsedArgs.verbose);
311
- generateModuleBootstrap(parsedArgs.pluginDir, manifest.moduleExec);
312
- validateDeclaredModuleExec(parsedArgs.pluginDir, manifest.moduleExec);
323
+ writeBuildProgress(`running build:plugin`);
324
+ runCommand((0, npm_runner_1.npmExecutable)(), [...(0, npm_runner_1.npmArgsPrefix)(), "run", "build:plugin"], parsedArgs.pluginDir, parsedArgs.verbose);
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}`);
313
330
  return manifest;
314
331
  }
package/lib/init.d.ts CHANGED
@@ -22,18 +22,22 @@ interface InitCommandError {
22
22
  export declare function initUsage(commandName?: string): string;
23
23
  declare function sanitizeIdToken(raw: string): string;
24
24
  declare function defaultPluginIdFromDir(targetDir: string): string;
25
+ declare function defaultPluginNameFromId(pluginId: string): string;
25
26
  declare function validatePluginId(pluginId: string): string | undefined;
26
27
  declare function createReadlinePromptDriver(output?: NodeJS.WritableStream): PromptDriver;
27
28
  declare function collectAnswers({ forceTheme, targetHint }: CollectAnswersOptions, promptDriver?: PromptDriver, output?: NodeJS.WritableStream): Promise<InitAnswers>;
28
29
  declare function writeModuleTemplate(answers: InitAnswers): void;
30
+ declare function writeThemeTemplate(answers: InitAnswers): void;
29
31
  export declare function runInitCommand(args: string[]): Promise<string>;
30
32
  export declare function isUsageError(error: unknown): error is InitCommandError;
31
33
  export declare const __private: {
32
34
  collectAnswers: typeof collectAnswers;
33
35
  createReadlinePromptDriver: typeof createReadlinePromptDriver;
36
+ defaultPluginNameFromId: typeof defaultPluginNameFromId;
34
37
  defaultPluginIdFromDir: typeof defaultPluginIdFromDir;
35
38
  sanitizeIdToken: typeof sanitizeIdToken;
36
39
  validatePluginId: typeof validatePluginId;
37
40
  writeModuleTemplate: typeof writeModuleTemplate;
41
+ writeThemeTemplate: typeof writeThemeTemplate;
38
42
  };
39
43
  export {};
package/lib/init.js CHANGED
@@ -42,17 +42,8 @@ const path = __importStar(require("node:path"));
42
42
  const readline = __importStar(require("node:readline/promises"));
43
43
  const node_process_1 = require("node:process");
44
44
  const node_child_process_1 = require("node:child_process");
45
+ const npm_runner_1 = require("./npm-runner");
45
46
  const packageJson = require("../package.json");
46
- function npmExecutable() {
47
- return "npm";
48
- }
49
- function shouldUseWindowsShell(program) {
50
- if (process.platform !== "win32") {
51
- return false;
52
- }
53
- const normalized = program.toLowerCase();
54
- return normalized === "npm" || normalized.endsWith(".cmd") || normalized.endsWith(".bat");
55
- }
56
47
  function initUsage(commandName = "openvcs") {
57
48
  return `Usage: ${commandName} init [--theme] [target-dir]\n\nOptions:\n --theme Start with a theme-only plugin template\n`;
58
49
  }
@@ -203,10 +194,10 @@ async function collectAnswers({ forceTheme, targetHint }, promptDriver = createR
203
194
  }
204
195
  }
205
196
  function runNpmInstall(targetDir) {
206
- const result = (0, node_child_process_1.spawnSync)(npmExecutable(), ["install"], {
197
+ const result = (0, node_child_process_1.spawnSync)((0, npm_runner_1.npmExecutable)(), [...(0, npm_runner_1.npmArgsPrefix)(), "install"], {
207
198
  cwd: targetDir,
208
- shell: shouldUseWindowsShell(npmExecutable()),
209
199
  stdio: "inherit",
200
+ windowsHide: true,
210
201
  });
211
202
  if (result.error) {
212
203
  throw new Error(`failed to spawn npm install in ${targetDir}: ${result.error.message}`);
@@ -228,7 +219,6 @@ function writeModuleTemplate(answers) {
228
219
  openvcs: {
229
220
  id: answers.pluginId,
230
221
  name: answers.pluginName,
231
- version: answers.pluginVersion,
232
222
  default_enabled: answers.defaultEnabled,
233
223
  module: { exec: "openvcs-plugin.js" },
234
224
  },
@@ -269,7 +259,6 @@ function writeThemeTemplate(answers) {
269
259
  openvcs: {
270
260
  id: answers.pluginId,
271
261
  name: answers.pluginName,
272
- version: answers.pluginVersion,
273
262
  default_enabled: answers.defaultEnabled,
274
263
  },
275
264
  scripts: {
@@ -350,8 +339,10 @@ function isUsageError(error) {
350
339
  exports.__private = {
351
340
  collectAnswers,
352
341
  createReadlinePromptDriver,
342
+ defaultPluginNameFromId,
353
343
  defaultPluginIdFromDir,
354
344
  sanitizeIdToken,
355
345
  validatePluginId,
356
346
  writeModuleTemplate,
347
+ writeThemeTemplate,
357
348
  };
@@ -0,0 +1,11 @@
1
+ type ExistsFn = (path: string) => boolean;
2
+ type ResolveFn = (specifier: string) => string;
3
+ export interface NpmCommand {
4
+ program: string;
5
+ argsPrefix: string[];
6
+ }
7
+ export declare function resolveNpmCli(execPath?: string, exists?: ExistsFn, resolve?: ResolveFn): string;
8
+ export declare function npmCommand(platform?: NodeJS.Platform, execPath?: string, exists?: ExistsFn, resolve?: ResolveFn): NpmCommand;
9
+ export declare function npmExecutable(): string;
10
+ export declare function npmArgsPrefix(): string[];
11
+ export {};
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ // Copyright © 2025-2026 OpenVCS Contributors
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || (function () {
21
+ var ownKeys = function(o) {
22
+ ownKeys = Object.getOwnPropertyNames || function (o) {
23
+ var ar = [];
24
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
25
+ return ar;
26
+ };
27
+ return ownKeys(o);
28
+ };
29
+ return function (mod) {
30
+ if (mod && mod.__esModule) return mod;
31
+ var result = {};
32
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
33
+ __setModuleDefault(result, mod);
34
+ return result;
35
+ };
36
+ })();
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.resolveNpmCli = resolveNpmCli;
39
+ exports.npmCommand = npmCommand;
40
+ exports.npmExecutable = npmExecutable;
41
+ exports.npmArgsPrefix = npmArgsPrefix;
42
+ const fs = __importStar(require("node:fs"));
43
+ const path = __importStar(require("node:path"));
44
+ function resolveNpmCli(execPath = process.execPath, exists = fs.existsSync, resolve = require.resolve) {
45
+ const localNodeModules = path.join(path.dirname(execPath), "node_modules", "npm", "bin", "npm-cli.js");
46
+ if (exists(localNodeModules)) {
47
+ return localNodeModules;
48
+ }
49
+ return resolve("npm/bin/npm-cli.js");
50
+ }
51
+ function npmCommand(platform = process.platform, execPath = process.execPath, exists = fs.existsSync, resolve = require.resolve) {
52
+ if (platform !== "win32") {
53
+ return { program: "npm", argsPrefix: [] };
54
+ }
55
+ return { program: execPath, argsPrefix: [resolveNpmCli(execPath, exists, resolve)] };
56
+ }
57
+ function npmExecutable() {
58
+ return npmCommand().program;
59
+ }
60
+ function npmArgsPrefix() {
61
+ return npmCommand().argsPrefix;
62
+ }
@@ -83,23 +83,31 @@ function createRuntimeDispatcher(options, host, writer) {
83
83
  }
84
84
  let result;
85
85
  if (timeout && timeout > 0) {
86
- const controller = new AbortController();
87
- const timeoutId = setTimeout(() => controller.abort(), timeout);
86
+ let timeoutId;
87
+ const timeoutPromise = new Promise((_resolve, reject) => {
88
+ timeoutId = setTimeout(() => {
89
+ reject((0, errors_1.pluginError)('request-timeout', `method '${method}' timed out after ${timeout}ms`));
90
+ }, timeout);
91
+ });
88
92
  try {
89
- result = await handler(params, {
90
- host,
91
- requestId: id,
92
- method,
93
- });
93
+ result = await Promise.race([
94
+ handler(params, {
95
+ host,
96
+ requestId: id,
97
+ method,
98
+ }),
99
+ timeoutPromise,
100
+ ]);
94
101
  }
95
102
  catch (error) {
96
- clearTimeout(timeoutId);
97
- if (error.name === 'AbortError') {
98
- throw (0, errors_1.pluginError)('request-timeout', `method '${method}' timed out after ${timeout}ms`);
103
+ if (timeoutId !== undefined) {
104
+ clearTimeout(timeoutId);
99
105
  }
100
106
  throw error;
101
107
  }
102
- clearTimeout(timeoutId);
108
+ if (timeoutId !== undefined) {
109
+ clearTimeout(timeoutId);
110
+ }
103
111
  }
104
112
  else {
105
113
  result = await handler(params, {
@@ -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<string[]>;
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',
@@ -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, string[], TContext>;
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`. */