vgxness 1.21.1 → 1.21.2

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.
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { canonicalCapabilitySubagentLegacyAliases } from '../../../domain/agents/canonical-agent-manifest.js';
4
4
  import { createOpenCodeDefaultAgentConfig, createOpenCodeDefaultAgentInstallPlan, } from './opencode-default-agent-config.js';
5
+ import { planVgxnessPathProfileUpdate, resolveVgxnessPathEnvironment, } from './vgxness-path-environment.js';
5
6
  const opencodeConfigSchema = 'https://opencode.ai/config.json';
6
7
  const opencodeUserGlobalOnlyMessage = 'VGX-managed OpenCode configuration is user-global only. Project/local OpenCode files are treated as external/manual diagnostics and will not be written by VGXNESS. Re-run without the project/local install scope.';
7
8
  export function resolveOpenCodeMcpInstallTarget(input) {
@@ -40,7 +41,7 @@ export function planOpenCodeMcpInstall(input) {
40
41
  if (scope === 'user') {
41
42
  return planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan, overwriteVgxness, bashPermissionPolicy);
42
43
  }
43
- return refusal('unsupported_config_shape', opencodeUserGlobalOnlyMessage, input.databasePath, databasePathSource, scope, undefined, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
44
+ return refusal('unsupported_config_shape', opencodeUserGlobalOnlyMessage, input.databasePath, databasePathSource, scope, undefined, [], agentPlan, overwriteVgxness, bashPermissionPolicy, input.env);
44
45
  }
45
46
  function planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan, overwriteVgxness, bashPermissionPolicy) {
46
47
  const target = resolveOpenCodeMcpInstallTarget({
@@ -49,15 +50,15 @@ function planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan
49
50
  env: input.env,
50
51
  });
51
52
  if (!target.ok) {
52
- return refusal('unsupported_config_shape', target.message, input.databasePath, databasePathSource, 'user', undefined, [], undefined, overwriteVgxness, bashPermissionPolicy);
53
+ return refusal('unsupported_config_shape', target.message, input.databasePath, databasePathSource, 'user', undefined, [], undefined, overwriteVgxness, bashPermissionPolicy, input.env);
53
54
  }
54
55
  const jsoncPath = `${target.path}c`;
55
56
  if (existsSync(jsoncPath)) {
56
- return refusal('unsupported_jsonc', 'OpenCode user JSONC config opencode.jsonc is not supported yet; use JSON or remove comments first.', input.databasePath, databasePathSource, 'user', jsoncPath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
57
+ return refusal('unsupported_jsonc', 'OpenCode user JSONC config opencode.jsonc is not supported yet; use JSON or remove comments first.', input.databasePath, databasePathSource, 'user', jsoncPath, [], agentPlan, overwriteVgxness, bashPermissionPolicy, input.env);
57
58
  }
58
59
  if (!existsSync(target.path)) {
59
60
  return {
60
- ...baseContract(input.databasePath, databasePathSource, 'user', target.path, false, 'create', agentPlan, overwriteVgxness, bashPermissionPolicy),
61
+ ...baseContract(input.databasePath, databasePathSource, 'user', target.path, false, 'create', agentPlan, overwriteVgxness, bashPermissionPolicy, input.env),
61
62
  status: 'would_install',
62
63
  action: 'create',
63
64
  targetPath: target.path,
@@ -69,13 +70,13 @@ function planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan
69
70
  }
70
71
  const parsed = parseConfig(target.path);
71
72
  if (!parsed.ok)
72
- return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
73
+ return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, bashPermissionPolicy, input.env);
73
74
  const config = parsed.value;
74
75
  if (config.mcp !== undefined && !isRecord(config.mcp)) {
75
- return refusal('invalid_mcp_shape', 'Existing top-level mcp must be a JSON object before vgxness can be merged.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
76
+ return refusal('invalid_mcp_shape', 'Existing top-level mcp must be a JSON object before vgxness can be merged.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, bashPermissionPolicy, input.env);
76
77
  }
77
78
  if (isRecord(config.mcp) && Object.hasOwn(config.mcp, 'vgxness') && !overwriteVgxness) {
78
- return refusal('existing_vgxness_mcp', 'Existing OpenCode config already contains mcp.vgxness; overwrite is refused by default.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
79
+ return refusal('existing_vgxness_mcp', 'Existing OpenCode config already contains mcp.vgxness; overwrite is refused by default.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, bashPermissionPolicy, input.env);
79
80
  }
80
81
  const agentConflict = findConflictingVgxnessAgents(config, agentPlan);
81
82
  if (agentConflict.length > 0 && !overwriteVgxness) {
@@ -84,16 +85,16 @@ function planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan
84
85
  kind: 'manual-check',
85
86
  message: `Manually reconcile conflicting VGXNESS agent entries: ${agentConflict.join(', ')}.`,
86
87
  },
87
- ], agentPlan, overwriteVgxness, bashPermissionPolicy);
88
+ ], agentPlan, overwriteVgxness, bashPermissionPolicy, input.env);
88
89
  }
89
90
  if (agentPlan.installsAgents && config.agent !== undefined && !isRecord(config.agent)) {
90
- return refusal('unsupported_config_shape', 'Existing top-level agent must be a JSON object before VGXNESS agent entries can be merged or overwritten.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
91
+ return refusal('unsupported_config_shape', 'Existing top-level agent must be a JSON object before VGXNESS agent entries can be merged or overwritten.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, bashPermissionPolicy, input.env);
91
92
  }
92
93
  if (config.permission !== undefined && !isRecord(config.permission)) {
93
- return refusal('unsupported_config_shape', 'Existing top-level permission must be a JSON object before VGXNESS can set top-level permission.bash to ask.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
94
+ return refusal('unsupported_config_shape', 'Existing top-level permission must be a JSON object before VGXNESS can set top-level permission.bash to ask.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, bashPermissionPolicy, input.env);
94
95
  }
95
96
  return {
96
- ...baseContract(input.databasePath, databasePathSource, 'user', target.path, true, 'merge-preserve-existing', agentPlan, overwriteVgxness, bashPermissionPolicy),
97
+ ...baseContract(input.databasePath, databasePathSource, 'user', target.path, true, 'merge-preserve-existing', agentPlan, overwriteVgxness, bashPermissionPolicy, input.env),
97
98
  status: 'would_install',
98
99
  action: 'merge',
99
100
  targetPath: target.path,
@@ -157,7 +158,9 @@ function deepEqual(left, right) {
157
158
  const rightKeys = Object.keys(right).sort();
158
159
  return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => key === rightKeys[index] && deepEqual(left[key], right[key]));
159
160
  }
160
- function baseContract(databasePath, source, scope, targetPath, backupRequired, mergePolicy, agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true }), overwriteVgxness = false, bashPermissionPolicy = bashPermissionPolicyFor(agentPlan)) {
161
+ function baseContract(databasePath, source, scope, targetPath, backupRequired, mergePolicy, agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true }), overwriteVgxness = false, bashPermissionPolicy = bashPermissionPolicyFor(agentPlan), env) {
162
+ const pathEnvironment = resolveVgxnessPathEnvironment({ env });
163
+ const pathProfileUpdate = planVgxnessPathProfileUpdate({ env });
161
164
  return {
162
165
  version: 1,
163
166
  kind: 'mcp-client-install-opencode',
@@ -172,18 +175,20 @@ function baseContract(databasePath, source, scope, targetPath, backupRequired, m
172
175
  mergePolicy,
173
176
  },
174
177
  scope,
175
- warnings: warningsForScope(scope, overwriteVgxness, agentPlan, bashPermissionPolicy),
176
- verificationHints: verificationHints(databasePath, source),
178
+ warnings: warningsForScope(scope, overwriteVgxness, agentPlan, bashPermissionPolicy, pathEnvironment),
179
+ verificationHints: verificationHints(databasePath, source, pathEnvironment),
177
180
  manualTest: manualTestForScope(scope, databasePath, source),
178
181
  installsAgents: agentPlan.installsAgents,
179
182
  agentNames: agentPlan.agentNames,
180
183
  overwriteVgxness,
181
184
  bashPermissionPolicy,
185
+ pathEnvironment,
186
+ ...(pathProfileUpdate.supported && pathProfileUpdate.changed ? { pathProfileUpdate } : {}),
182
187
  ...(agentPlan.defaultAgent !== undefined ? { defaultAgent: agentPlan.defaultAgent } : {}),
183
188
  };
184
189
  }
185
- function refusal(reason, message, databasePath, source, scope, targetPath, extraVerificationHints = [], agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true }), overwriteVgxness = false, bashPermissionPolicy = bashPermissionPolicyFor(agentPlan)) {
186
- const contract = baseContract(databasePath, source, scope, targetPath, false, 'refuse-no-clobber', agentPlan, overwriteVgxness, bashPermissionPolicy);
190
+ function refusal(reason, message, databasePath, source, scope, targetPath, extraVerificationHints = [], agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true }), overwriteVgxness = false, bashPermissionPolicy = bashPermissionPolicyFor(agentPlan), env) {
191
+ const contract = baseContract(databasePath, source, scope, targetPath, false, 'refuse-no-clobber', agentPlan, overwriteVgxness, bashPermissionPolicy, env);
187
192
  return {
188
193
  ...contract,
189
194
  verificationHints: [...extraVerificationHints, ...contract.verificationHints],
@@ -193,7 +198,7 @@ function refusal(reason, message, databasePath, source, scope, targetPath, extra
193
198
  ...(targetPath !== undefined ? { targetPath } : {}),
194
199
  };
195
200
  }
196
- function warningsForScope(_scope, overwriteVgxness, agentPlan, bashPermissionPolicy) {
201
+ function warningsForScope(_scope, overwriteVgxness, agentPlan, bashPermissionPolicy, pathEnvironment) {
197
202
  const overwriteWarnings = overwriteVgxness
198
203
  ? [
199
204
  `Reinstall/overwrite is enabled: existing VGXNESS-managed OpenCode entries will be replaced (${agentPlan.installsAgents ? 'mcp.vgxness, default_agent, instructions AGENTS.md, and known VGXNESS agents' : 'mcp.vgxness only'}); unrelated OpenCode config is preserved.`,
@@ -207,6 +212,7 @@ function warningsForScope(_scope, overwriteVgxness, agentPlan, bashPermissionPol
207
212
  return [
208
213
  'Restart OpenCode after installation so it reloads the user MCP config.',
209
214
  'OpenCode project config may override user config for a workspace; check project-level config if vgxness is not visible.',
215
+ ...pathEnvironment.warnings,
210
216
  ...overwriteWarnings,
211
217
  ...bashWarnings,
212
218
  ];
@@ -225,7 +231,7 @@ function manualTestForScope(_scope, databasePath, source) {
225
231
  doctorCommand: createVgxnessMcpDoctorCommand(databasePath, source),
226
232
  };
227
233
  }
228
- function verificationHints(databasePath, source) {
234
+ function verificationHints(databasePath, source, pathEnvironment) {
229
235
  return [
230
236
  {
231
237
  kind: 'restart-client',
@@ -240,5 +246,6 @@ function verificationHints(databasePath, source) {
240
246
  message: 'Run the MCP doctor command after installation.',
241
247
  command: createVgxnessMcpDoctorCommand(databasePath, source),
242
248
  },
249
+ ...(pathEnvironment.vgxness.onPath ? [] : pathEnvironment.remediation.map((message) => ({ kind: 'manual-check', message }))),
243
250
  ];
244
251
  }
@@ -3,6 +3,7 @@ import { dirname, join } from 'node:path';
3
3
  import { canonicalCapabilitySubagentLegacyAliases } from '../../../domain/agents/canonical-agent-manifest.js';
4
4
  import { createManagedProviderConfigBackup } from '../../../setup/backup-rollback-service.js';
5
5
  import { findConflictingVgxnessAgents, planOpenCodeMcpInstall, } from './client-install-opencode-contract.js';
6
+ import { applyVgxnessPathProfileUpdate } from './vgxness-path-environment.js';
6
7
  import { createOpenCodeDefaultAgentConfig, createOpenCodeDefaultAgentInstallPlan, createOpenCodeGlobalInstructionsContent, createOpenCodeGlobalInstructionsManagedBlock, vgxnessOpenCodeGlobalInstructionsBlockEnd, vgxnessOpenCodeGlobalInstructionsBlockStart, vgxnessOpenCodeInstructionsPath, } from './opencode-default-agent-config.js';
7
8
  const opencodeConfigSchema = 'https://opencode.ai/config.json';
8
9
  export { createInstalledVgxnessMcpServerCommand };
@@ -50,7 +51,8 @@ export async function installOpenCodeMcpClient(input) {
50
51
  return refusal('post_write_validation_failed', globalInstructions.error.message, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.bashPermissionPolicy);
51
52
  writeConfig(plan.targetPath, mergeVgxnessOpenCodeConfig({ $schema: opencodeConfigSchema }, plan.server, plan.installsAgents, input.effectiveManagerInstructions));
52
53
  writeOpenCodeGlobalInstructions(globalInstructions.value);
53
- return validateInstalledResult(plan.targetPath, undefined, server, input.databasePath, databasePathSource, applySafety(plan), plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.bashPermissionPolicy);
54
+ const pathProfileUpdate = applyVgxnessPathProfileUpdate({ env: input.env });
55
+ return validateInstalledResult(plan.targetPath, undefined, server, input.databasePath, databasePathSource, applySafety(plan), plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.bashPermissionPolicy, plan.pathEnvironment, pathProfileUpdate);
54
56
  }
55
57
  const parsed = parseConfig(plan.targetPath);
56
58
  if (!parsed.ok)
@@ -70,7 +72,8 @@ export async function installOpenCodeMcpClient(input) {
70
72
  const mergedConfig = mergeVgxnessOpenCodeConfig({ ...config, $schema: plan.existingSchema ?? opencodeConfigSchema }, plan.server, plan.installsAgents, input.effectiveManagerInstructions);
71
73
  writeConfig(plan.targetPath, mergedConfig);
72
74
  writeOpenCodeGlobalInstructions(globalInstructions.value);
73
- return validateInstalledResult(plan.targetPath, backup.value, server, input.databasePath, databasePathSource, applySafety(plan), plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.bashPermissionPolicy);
75
+ const pathProfileUpdate = applyVgxnessPathProfileUpdate({ env: input.env });
76
+ return validateInstalledResult(plan.targetPath, backup.value, server, input.databasePath, databasePathSource, applySafety(plan), plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.bashPermissionPolicy, plan.pathEnvironment, pathProfileUpdate);
74
77
  }
75
78
  function mergeVgxnessOpenCodeConfig(config, server, installsAgents, effectiveManagerInstructions) {
76
79
  const merged = {
@@ -174,7 +177,7 @@ function createBackup(path, scope, allowedManagedHome, override) {
174
177
  ...(allowedManagedHome !== undefined ? { allowedManagedHome } : {}),
175
178
  });
176
179
  }
177
- function validateInstalledResult(targetPath, backup, server, databasePath, source, safety, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness = false, bashPermissionPolicy = bashPermissionPolicyFor(agentPlan)) {
180
+ function validateInstalledResult(targetPath, backup, server, databasePath, source, safety, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness = false, bashPermissionPolicy = bashPermissionPolicyFor(agentPlan), pathEnvironment, pathProfileUpdate) {
178
181
  const parsed = parseConfig(targetPath);
179
182
  if (!parsed.ok)
180
183
  return refusal('post_write_validation_failed', 'OpenCode config could not be re-read after write.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness, bashPermissionPolicy);
@@ -212,6 +215,8 @@ function validateInstalledResult(targetPath, backup, server, databasePath, sourc
212
215
  agentNames: agentPlan.agentNames,
213
216
  overwriteVgxness,
214
217
  bashPermissionPolicy,
218
+ pathEnvironment,
219
+ ...(pathProfileUpdate.supported ? { pathProfileUpdate } : {}),
215
220
  ...(agentPlan.defaultAgent !== undefined ? { defaultAgent: agentPlan.defaultAgent } : {}),
216
221
  };
217
222
  }
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { dirname, join, relative, resolve } from 'node:path';
3
3
  import { SUPPORTED_VGX_MCP_TOOL_NAMES } from '../../../mcp/schema.js';
4
4
  import { resolveOpenCodeMcpInstallTarget } from './client-install-opencode-contract.js';
5
+ import { resolveVgxnessPathEnvironment } from './vgxness-path-environment.js';
5
6
  const serverName = 'vgxness';
6
7
  const expectedToolCount = SUPPORTED_VGX_MCP_TOOL_NAMES.length;
7
8
  export function createOpenCodeMcpVisibilityReport(options) {
@@ -17,6 +18,7 @@ export function createOpenCodeMcpVisibilityReport(options) {
17
18
  const userReport = userTarget.ok ? userVisibilityReport(userTarget.path, projectVisibleFromCurrentCwd) : undefined;
18
19
  const selectedTargetPath = userTarget.ok ? userTarget.path : '$HOME/.config/opencode/opencode.json';
19
20
  const selectedReady = targetScope === 'user' ? userReport?.configExists === true && userReport.hasVgxnessServer : projectVisibleFromCurrentCwd;
21
+ const pathEnvironment = resolveVgxnessPathEnvironment({ env: options.env });
20
22
  return {
21
23
  version: 1,
22
24
  kind: 'mcp-doctor-opencode-visibility',
@@ -36,6 +38,7 @@ export function createOpenCodeMcpVisibilityReport(options) {
36
38
  },
37
39
  visibility: { cwdInsideProject, configExists: projectConfigExists, hasVgxnessServer: projectHasVgxnessServer },
38
40
  expected: { serverName, toolCount: expectedToolCount, visibleFromCurrentCwd: selectedReady },
41
+ pathEnvironment,
39
42
  guidance: buildGuidance({
40
43
  cwdInsideProject,
41
44
  configExists: projectConfigExists,
@@ -0,0 +1,186 @@
1
+ import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
+ import { delimiter, dirname, extname, join, resolve } from 'node:path';
3
+ export const vgxnessPathProfileBlockStart = '# >>> VGXNESS managed PATH >>>';
4
+ export const vgxnessPathProfileBlockEnd = '# <<< VGXNESS managed PATH <<<';
5
+ export function resolveVgxnessPathEnvironment(input) {
6
+ const env = input.env ?? process.env;
7
+ const home = homeFromEnv(env);
8
+ const pathEntries = splitPath(Object.hasOwn(env, 'PATH') ? env.PATH : process.env.PATH, home);
9
+ const bunGlobalBinPath = bunGlobalBin(env, home);
10
+ const vgxness = resolveCommandOnPath('vgxness', pathEntries);
11
+ const opencodeCli = resolveCommandOnPath('opencode', pathEntries);
12
+ const bunGlobalBinStatus = {
13
+ path: bunGlobalBinPath,
14
+ exists: existsSync(bunGlobalBinPath),
15
+ onPath: pathEntries.some((entry) => samePath(entry, bunGlobalBinPath)),
16
+ hasVgxnessExecutable: resolveCommandInDirectory('vgxness', bunGlobalBinPath).onPath,
17
+ };
18
+ const warnings = pathWarnings(vgxness, bunGlobalBinStatus);
19
+ return {
20
+ command: 'vgxness',
21
+ pathEntries,
22
+ vgxness,
23
+ opencodeCli,
24
+ bunGlobalBin: bunGlobalBinStatus,
25
+ warnings,
26
+ remediation: pathRemediation(vgxness, bunGlobalBinStatus, opencodeCli),
27
+ };
28
+ }
29
+ export function planVgxnessPathProfileUpdate(input) {
30
+ const env = input.env ?? process.env;
31
+ const home = homeFromEnv(env);
32
+ const targetBinDir = bunGlobalBin(env, home);
33
+ const managedBlock = createManagedPathBlock(targetBinDir, home);
34
+ if (home === undefined) {
35
+ return {
36
+ supported: false,
37
+ changed: false,
38
+ backupRequired: false,
39
+ managedBlock,
40
+ reason: 'HOME is not set; VGXNESS cannot safely update a shell profile PATH block.',
41
+ targetBinDir,
42
+ };
43
+ }
44
+ const profilePath = shellProfilePath(env, home);
45
+ const existing = existsSync(profilePath) ? readFileSync(profilePath, 'utf8') : undefined;
46
+ const nextContent = reconcileManagedPathBlock(existing, managedBlock);
47
+ const pathEnvironment = resolveVgxnessPathEnvironment({ env });
48
+ const blockExists = existing?.includes(vgxnessPathProfileBlockStart) === true || existing?.includes(vgxnessPathProfileBlockEnd) === true;
49
+ const shouldEnsureProfile = blockExists || (!pathEnvironment.vgxness.onPath && !pathEnvironment.bunGlobalBin.onPath);
50
+ const changed = shouldEnsureProfile && existing !== nextContent;
51
+ return {
52
+ supported: true,
53
+ changed,
54
+ backupRequired: changed && existing !== undefined,
55
+ managedBlock,
56
+ reason: changed
57
+ ? 'A VGXNESS-managed shell PATH block is planned so path-based OpenCode MCP command `vgxness mcp start` can resolve after restarting terminal/OpenCode.'
58
+ : 'No shell profile PATH update is needed for the current environment.',
59
+ profilePath,
60
+ targetBinDir,
61
+ };
62
+ }
63
+ export function applyVgxnessPathProfileUpdate(input) {
64
+ const plan = planVgxnessPathProfileUpdate(input);
65
+ if (!plan.supported || !plan.changed || plan.profilePath === undefined)
66
+ return plan;
67
+ const existing = existsSync(plan.profilePath) ? readFileSync(plan.profilePath, 'utf8') : undefined;
68
+ const backupPath = existing === undefined ? undefined : createProfileBackup(plan.profilePath);
69
+ if (backupPath !== undefined)
70
+ copyFileSync(plan.profilePath, backupPath);
71
+ mkdirSync(dirname(plan.profilePath), { recursive: true });
72
+ writeFileSync(plan.profilePath, reconcileManagedPathBlock(existing, plan.managedBlock));
73
+ return { ...plan, ...(backupPath === undefined ? {} : { backupPath }) };
74
+ }
75
+ function pathWarnings(vgxness, bunGlobalBinStatus) {
76
+ if (vgxness.onPath)
77
+ return [];
78
+ return [
79
+ `PATH-based OpenCode MCP command \`vgxness mcp start\` may fail because \`vgxness\` was not found on the current PATH. OpenCode may report "Executable not found in $PATH" until ${bunGlobalBinStatus.path} is on PATH.`,
80
+ ];
81
+ }
82
+ function pathRemediation(vgxness, bunGlobalBinStatus, opencodeCli) {
83
+ const steps = vgxness.onPath
84
+ ? ['`vgxness` resolves from the current PATH; keep OpenCode config path-based as `vgxness mcp start`.']
85
+ : [
86
+ 'Keep OpenCode config portable as `mcp.vgxness.command: ["vgxness", "mcp", "start"]`; do not replace it with an absolute binary path by default.',
87
+ `Add Bun global binaries to your shell PATH, for example: export PATH="$HOME/.bun/bin:$PATH" (${bunGlobalBinStatus.path}).`,
88
+ 'Restart terminal/OpenCode after PATH profile changes so the provider process inherits the updated PATH.',
89
+ ];
90
+ return opencodeCli.onPath
91
+ ? [...steps, 'After restarting, verify with `opencode mcp list` and confirm the `vgxness` server is listed.']
92
+ : [...steps, 'Skipped `opencode mcp list` verification because the OpenCode CLI was not found on the current PATH.'];
93
+ }
94
+ function createManagedPathBlock(targetBinDir, home) {
95
+ const pathExpression = home !== undefined && samePath(targetBinDir, join(home, '.bun', 'bin')) ? '$HOME/.bun/bin' : targetBinDir;
96
+ return [
97
+ vgxnessPathProfileBlockStart,
98
+ '# Ensures OpenCode can resolve the path-based VGXNESS MCP command.',
99
+ `export PATH="${pathExpression}:$PATH"`,
100
+ vgxnessPathProfileBlockEnd,
101
+ ].join('\n');
102
+ }
103
+ function reconcileManagedPathBlock(existing, managedBlock) {
104
+ if (existing === undefined || existing.length === 0)
105
+ return `${managedBlock}\n`;
106
+ const start = existing.indexOf(vgxnessPathProfileBlockStart);
107
+ const end = existing.indexOf(vgxnessPathProfileBlockEnd);
108
+ if (start >= 0 && end >= start) {
109
+ const afterEnd = end + vgxnessPathProfileBlockEnd.length;
110
+ return `${existing.slice(0, start).trimEnd()}\n\n${managedBlock}\n${existing.slice(afterEnd).trimStart()}`;
111
+ }
112
+ return `${existing.trimEnd()}\n\n${managedBlock}\n`;
113
+ }
114
+ function shellProfilePath(env, home) {
115
+ const explicit = env.VGXNESS_SHELL_PROFILE;
116
+ if (explicit !== undefined && explicit.trim().length > 0)
117
+ return resolve(home, explicit);
118
+ const shell = env.SHELL ?? '';
119
+ if (shell.endsWith('/zsh') || shell === 'zsh')
120
+ return join(home, '.zshrc');
121
+ if (shell.endsWith('/bash') || shell === 'bash')
122
+ return join(home, '.bashrc');
123
+ return join(home, '.profile');
124
+ }
125
+ function createProfileBackup(profilePath) {
126
+ const stamp = new Date().toISOString().replaceAll('-', '').replaceAll(':', '').replace(/\.\d{3}Z$/, 'Z');
127
+ return `${profilePath}.backup-${stamp}-vgxness-path`;
128
+ }
129
+ function bunGlobalBin(env, home) {
130
+ const bunInstall = env.BUN_INSTALL;
131
+ if (bunInstall !== undefined && bunInstall.trim().length > 0)
132
+ return resolve(expandHome(bunInstall, home), 'bin');
133
+ return home === undefined ? resolve('.bun', 'bin') : join(home, '.bun', 'bin');
134
+ }
135
+ function homeFromEnv(env) {
136
+ const home = env.HOME;
137
+ return home === undefined || home.trim().length === 0 ? undefined : home;
138
+ }
139
+ function splitPath(pathValue, home) {
140
+ if (pathValue === undefined || pathValue.length === 0)
141
+ return [];
142
+ return pathValue
143
+ .split(delimiter)
144
+ .filter((entry) => entry.length > 0)
145
+ .map((entry) => resolve(expandHome(entry, home)));
146
+ }
147
+ function expandHome(pathValue, home) {
148
+ if (home !== undefined && (pathValue === '~' || pathValue.startsWith('~/')))
149
+ return join(home, pathValue.slice(2));
150
+ return pathValue;
151
+ }
152
+ function resolveCommandOnPath(command, pathEntries) {
153
+ for (const pathEntry of pathEntries) {
154
+ const resolved = resolveCommandInDirectory(command, pathEntry);
155
+ if (resolved.onPath)
156
+ return resolved;
157
+ }
158
+ return { onPath: false };
159
+ }
160
+ function resolveCommandInDirectory(command, directory) {
161
+ for (const candidateName of commandCandidateNames(command)) {
162
+ const candidate = join(directory, candidateName);
163
+ if (isExecutable(candidate))
164
+ return { onPath: true, resolvedPath: candidate };
165
+ }
166
+ return { onPath: false };
167
+ }
168
+ function commandCandidateNames(command) {
169
+ if (process.platform !== 'win32' || extname(command).length > 0)
170
+ return [command];
171
+ return [command, `${command}.cmd`, `${command}.exe`, `${command}.bat`, `${command}.ps1`];
172
+ }
173
+ function isExecutable(path) {
174
+ try {
175
+ accessSync(path, process.platform === 'win32' ? constants.F_OK : constants.X_OK);
176
+ return statSync(path).isFile();
177
+ }
178
+ catch {
179
+ return false;
180
+ }
181
+ }
182
+ function samePath(left, right) {
183
+ const resolvedLeft = resolve(left);
184
+ const resolvedRight = resolve(right);
185
+ return process.platform === 'win32' ? resolvedLeft.toLowerCase() === resolvedRight.toLowerCase() : resolvedLeft === resolvedRight;
186
+ }
@@ -22,6 +22,11 @@ function renderOpenCode(plan) {
22
22
  field('Installs agents', yesNo(plan.opencode.installsAgents)),
23
23
  field('Reinstall entries', yesNo(plan.opencode.overwriteVgxness === true)),
24
24
  field('Bash policy', formatBashPermissionPolicy(plan.opencode.bashPermissionPolicy)),
25
+ field('VGXNESS on PATH', yesNo(plan.opencode.pathEnvironment.vgxness.onPath)),
26
+ field('Bun bin on PATH', yesNo(plan.opencode.pathEnvironment.bunGlobalBin.onPath)),
27
+ ...(plan.opencode.pathProfileUpdate === undefined
28
+ ? []
29
+ : [field('PATH profile', `${plan.opencode.pathProfileUpdate.changed ? 'planned' : 'unchanged'} — ${plan.opencode.pathProfileUpdate.profilePath ?? 'not supported'}`)]),
25
30
  field('Agent names', plan.opencode.agentNames.length === 0 ? 'none' : plan.opencode.agentNames.join(', ')),
26
31
  ], { status });
27
32
  }
@@ -141,6 +141,8 @@ function setupPlanFromOpenCode(input) {
141
141
  agentNames: input.opencode.agentNames,
142
142
  ...(input.opencode.overwriteVgxness ? { overwriteVgxness: true } : {}),
143
143
  bashPermissionPolicy: input.opencode.bashPermissionPolicy,
144
+ pathEnvironment: input.opencode.pathEnvironment,
145
+ ...(input.opencode.pathProfileUpdate === undefined ? {} : { pathProfileUpdate: input.opencode.pathProfileUpdate }),
144
146
  },
145
147
  actions: [],
146
148
  conflicts: [
@@ -171,6 +173,8 @@ function setupPlanFromOpenCode(input) {
171
173
  agentNames: input.opencode.agentNames,
172
174
  ...(input.opencode.overwriteVgxness ? { overwriteVgxness: true } : {}),
173
175
  bashPermissionPolicy: input.opencode.bashPermissionPolicy,
176
+ pathEnvironment: input.opencode.pathEnvironment,
177
+ ...(input.opencode.pathProfileUpdate === undefined ? {} : { pathProfileUpdate: input.opencode.pathProfileUpdate }),
174
178
  },
175
179
  actions: [
176
180
  {
@@ -180,18 +184,22 @@ function setupPlanFromOpenCode(input) {
180
184
  targetPath: input.opencode.targetPath,
181
185
  backupRequired: input.opencode.backupRequired,
182
186
  },
187
+ ...pathProfileUpdateActions(input.opencode.pathProfileUpdate),
183
188
  ],
184
189
  conflicts: [],
185
- backupsPlanned: input.opencode.backupRequired
186
- ? [
187
- {
188
- targetPath: input.opencode.targetPath,
189
- reason: input.opencode.overwriteVgxness
190
- ? 'Existing OpenCode config would receive a managed VGXNESS backup with rollback metadata before VGXNESS entries are overwritten/reinstalled.'
191
- : 'Existing OpenCode config would receive a managed VGXNESS backup with rollback metadata before merge.',
192
- },
193
- ]
194
- : [],
190
+ backupsPlanned: [
191
+ ...(input.opencode.backupRequired
192
+ ? [
193
+ {
194
+ targetPath: input.opencode.targetPath,
195
+ reason: input.opencode.overwriteVgxness
196
+ ? 'Existing OpenCode config would receive a managed VGXNESS backup with rollback metadata before VGXNESS entries are overwritten/reinstalled.'
197
+ : 'Existing OpenCode config would receive a managed VGXNESS backup with rollback metadata before merge.',
198
+ },
199
+ ]
200
+ : []),
201
+ ...pathProfileUpdateBackups(input.opencode.pathProfileUpdate),
202
+ ],
195
203
  nextCommands: [
196
204
  input.opencode.overwriteVgxness ? 'vgxness setup reinstall --yes' : 'vgxness setup apply --yes',
197
205
  'vgxness doctor',
@@ -202,6 +210,29 @@ function setupPlanFromOpenCode(input) {
202
210
  function bashPermissionDescription(policy) {
203
211
  return policy.manager === 'deny' ? '; sets top-level permission.bash to ask and manager bash to deny' : '; sets top-level permission.bash to ask';
204
212
  }
213
+ function pathProfileUpdateActions(update) {
214
+ if (update === undefined || !update.supported || !update.changed || update.profilePath === undefined)
215
+ return [];
216
+ return [
217
+ {
218
+ id: 'opencode-path-profile-update',
219
+ description: `${update.reason} Target shell profile: ${update.profilePath}`,
220
+ mutating: false,
221
+ targetPath: update.profilePath,
222
+ backupRequired: update.backupRequired,
223
+ },
224
+ ];
225
+ }
226
+ function pathProfileUpdateBackups(update) {
227
+ if (update === undefined || !update.supported || !update.changed || !update.backupRequired || update.profilePath === undefined)
228
+ return [];
229
+ return [
230
+ {
231
+ targetPath: update.profilePath,
232
+ reason: 'Existing shell profile would receive a managed VGXNESS backup before updating the PATH block used by the path-based OpenCode MCP command.',
233
+ },
234
+ ];
235
+ }
205
236
  function resolveSetupDatabase(input) {
206
237
  if (input.mode === 'global') {
207
238
  const resolved = resolveMemoryDatabasePath({ cwd: input.workspaceRoot, env: input.env });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vgxness",
3
- "version": "1.21.1",
3
+ "version": "1.21.2",
4
4
  "description": "Local-first governance OS, CLI and MCP control plane for existing coding agents, SDD, memory, runs, permissions, and OpenCode setup.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {