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.
- package/dist/adapters/opencode/install/client-install-opencode-contract.js +25 -18
- package/dist/adapters/opencode/install/client-install-opencode.js +8 -3
- package/dist/adapters/opencode/install/opencode-visibility.js +3 -0
- package/dist/adapters/opencode/install/vgxness-path-environment.js +186 -0
- package/dist/interfaces/cli/setup-plan-renderer.js +5 -0
- package/dist/setup/setup-plan.js +41 -10
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/setup/setup-plan.js
CHANGED
|
@@ -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:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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.
|
|
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": {
|