vgxness 1.21.0 → 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 +79 -8
- package/dist/adapters/opencode/install/opencode-default-agent-config.js +89 -0
- 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/backup-rollback-service.js +2 -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
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { dirname } from 'node:path';
|
|
2
|
+
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 {
|
|
6
|
+
import { applyVgxnessPathProfileUpdate } from './vgxness-path-environment.js';
|
|
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 };
|
|
9
10
|
function createInstalledVgxnessMcpServerCommand(databasePath, source = 'flag') {
|
|
@@ -45,8 +46,13 @@ export async function installOpenCodeMcpClient(input) {
|
|
|
45
46
|
return agentConflictRefusal(conflictingAgents, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.bashPermissionPolicy);
|
|
46
47
|
return refusal('unsupported_config_shape', 'OpenCode config appeared after planning; rerun setup apply so it can be merged safely without overwriting user config.', input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.bashPermissionPolicy);
|
|
47
48
|
}
|
|
49
|
+
const globalInstructions = prepareOpenCodeGlobalInstructionsWrite(plan.targetPath, plan.scope, input.env?.HOME, agentPlan);
|
|
50
|
+
if (!globalInstructions.ok)
|
|
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);
|
|
48
52
|
writeConfig(plan.targetPath, mergeVgxnessOpenCodeConfig({ $schema: opencodeConfigSchema }, plan.server, plan.installsAgents, input.effectiveManagerInstructions));
|
|
49
|
-
|
|
53
|
+
writeOpenCodeGlobalInstructions(globalInstructions.value);
|
|
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);
|
|
50
56
|
}
|
|
51
57
|
const parsed = parseConfig(plan.targetPath);
|
|
52
58
|
if (!parsed.ok)
|
|
@@ -59,10 +65,15 @@ export async function installOpenCodeMcpClient(input) {
|
|
|
59
65
|
const backup = createBackup(plan.targetPath, plan.scope, input.env?.HOME);
|
|
60
66
|
if (!backup.ok)
|
|
61
67
|
return refusal('post_write_validation_failed', backup.error.message, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.bashPermissionPolicy);
|
|
68
|
+
const globalInstructions = prepareOpenCodeGlobalInstructionsWrite(plan.targetPath, plan.scope, input.env?.HOME, agentPlan);
|
|
69
|
+
if (!globalInstructions.ok)
|
|
70
|
+
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);
|
|
62
71
|
const config = parsed.value;
|
|
63
72
|
const mergedConfig = mergeVgxnessOpenCodeConfig({ ...config, $schema: plan.existingSchema ?? opencodeConfigSchema }, plan.server, plan.installsAgents, input.effectiveManagerInstructions);
|
|
64
73
|
writeConfig(plan.targetPath, mergedConfig);
|
|
65
|
-
|
|
74
|
+
writeOpenCodeGlobalInstructions(globalInstructions.value);
|
|
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);
|
|
66
77
|
}
|
|
67
78
|
function mergeVgxnessOpenCodeConfig(config, server, installsAgents, effectiveManagerInstructions) {
|
|
68
79
|
const merged = {
|
|
@@ -101,18 +112,72 @@ function writeConfig(path, config) {
|
|
|
101
112
|
mkdirSync(dirname(path), { recursive: true });
|
|
102
113
|
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
103
114
|
}
|
|
104
|
-
function
|
|
115
|
+
function prepareOpenCodeGlobalInstructionsWrite(opencodeConfigPath, scope, allowedManagedHome, agentPlan) {
|
|
116
|
+
const instructionsPath = join(dirname(opencodeConfigPath), vgxnessOpenCodeInstructionsPath);
|
|
117
|
+
if (!agentPlan.installsAgents)
|
|
118
|
+
return { ok: true, value: { path: instructionsPath, content: '', changed: false } };
|
|
119
|
+
const existing = existsSync(instructionsPath) ? readFileSync(instructionsPath, 'utf8') : undefined;
|
|
120
|
+
const nextContent = reconcileOpenCodeGlobalInstructions(existing);
|
|
121
|
+
if (existing === nextContent)
|
|
122
|
+
return { ok: true, value: { path: instructionsPath, content: nextContent, changed: false } };
|
|
123
|
+
if (existing !== undefined) {
|
|
124
|
+
const backup = createBackup(instructionsPath, scope, allowedManagedHome, {
|
|
125
|
+
reason: 'pre-global-instructions-merge',
|
|
126
|
+
description: 'Backup existing OpenCode global AGENTS.md before aligning VGXNESS global instructions.',
|
|
127
|
+
});
|
|
128
|
+
if (!backup.ok)
|
|
129
|
+
return backup;
|
|
130
|
+
}
|
|
131
|
+
return { ok: true, value: { path: instructionsPath, content: nextContent, changed: true } };
|
|
132
|
+
}
|
|
133
|
+
function writeOpenCodeGlobalInstructions(plan) {
|
|
134
|
+
if (!plan.changed)
|
|
135
|
+
return;
|
|
136
|
+
mkdirSync(dirname(plan.path), { recursive: true });
|
|
137
|
+
writeFileSync(plan.path, plan.content);
|
|
138
|
+
}
|
|
139
|
+
function reconcileOpenCodeGlobalInstructions(existing) {
|
|
140
|
+
const canonical = createOpenCodeGlobalInstructionsContent();
|
|
141
|
+
if (existing === undefined || existing.trim().length === 0)
|
|
142
|
+
return canonical;
|
|
143
|
+
if (looksLikeLegacyVgxnessGlobalInstructions(existing) || looksLikeUnmarkedVgxnessGlobalInstructions(existing))
|
|
144
|
+
return canonical;
|
|
145
|
+
if (existing.includes(vgxnessOpenCodeGlobalInstructionsBlockStart) || existing.includes(vgxnessOpenCodeGlobalInstructionsBlockEnd)) {
|
|
146
|
+
return replaceOpenCodeGlobalInstructionsManagedBlock(existing);
|
|
147
|
+
}
|
|
148
|
+
return `${existing.trimEnd()}\n\n${createOpenCodeGlobalInstructionsManagedBlock()}\n`;
|
|
149
|
+
}
|
|
150
|
+
function replaceOpenCodeGlobalInstructionsManagedBlock(existing) {
|
|
151
|
+
const start = existing.indexOf(vgxnessOpenCodeGlobalInstructionsBlockStart);
|
|
152
|
+
const end = existing.indexOf(vgxnessOpenCodeGlobalInstructionsBlockEnd);
|
|
153
|
+
const block = createOpenCodeGlobalInstructionsManagedBlock();
|
|
154
|
+
if (start < 0 || end < start)
|
|
155
|
+
return `${existing.trimEnd()}\n\n${block}\n`;
|
|
156
|
+
const afterEnd = end + vgxnessOpenCodeGlobalInstructionsBlockEnd.length;
|
|
157
|
+
return `${existing.slice(0, start).trimEnd()}\n\n${block}\n${existing.slice(afterEnd).trimStart()}`;
|
|
158
|
+
}
|
|
159
|
+
function looksLikeLegacyVgxnessGlobalInstructions(content) {
|
|
160
|
+
return /^# VGXNESS Persona\b/m.test(content) || content.includes('gentle-ai:codegraph-guidance');
|
|
161
|
+
}
|
|
162
|
+
function looksLikeUnmarkedVgxnessGlobalInstructions(content) {
|
|
163
|
+
return (/^# Global OpenCode Instructions\b/m.test(content) &&
|
|
164
|
+
content.includes('## Instruction Layering') &&
|
|
165
|
+
content.includes('## Memory and Context Continuity') &&
|
|
166
|
+
content.includes('## VGXNESS Workflow Expectations') &&
|
|
167
|
+
content.includes('## Code Context Providers'));
|
|
168
|
+
}
|
|
169
|
+
function createBackup(path, scope, allowedManagedHome, override) {
|
|
105
170
|
return createManagedProviderConfigBackup({
|
|
106
171
|
targetPath: path,
|
|
107
172
|
provider: 'opencode',
|
|
108
173
|
scope,
|
|
109
174
|
createdByOperation: 'mcp-client-install-opencode',
|
|
110
|
-
reason: 'pre-merge-safety',
|
|
111
|
-
description: 'Backup existing OpenCode config before merging VGXNESS MCP configuration.',
|
|
175
|
+
reason: override?.reason ?? 'pre-merge-safety',
|
|
176
|
+
description: override?.description ?? 'Backup existing OpenCode config before merging VGXNESS MCP configuration.',
|
|
112
177
|
...(allowedManagedHome !== undefined ? { allowedManagedHome } : {}),
|
|
113
178
|
});
|
|
114
179
|
}
|
|
115
|
-
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) {
|
|
116
181
|
const parsed = parseConfig(targetPath);
|
|
117
182
|
if (!parsed.ok)
|
|
118
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);
|
|
@@ -126,6 +191,10 @@ function validateInstalledResult(targetPath, backup, server, databasePath, sourc
|
|
|
126
191
|
if (!isRecord(agent) || parsed.value.default_agent !== agentPlan.defaultAgent || agentPlan.agentNames.some((name) => !isRecord(agent[name]))) {
|
|
127
192
|
return refusal('post_write_validation_failed', 'OpenCode config was written but VGXNESS agent entries did not validate.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
128
193
|
}
|
|
194
|
+
const instructionsPath = join(dirname(targetPath), vgxnessOpenCodeInstructionsPath);
|
|
195
|
+
if (!existsSync(instructionsPath) || !readFileSync(instructionsPath, 'utf8').includes('## Memory and Context Continuity')) {
|
|
196
|
+
return refusal('post_write_validation_failed', 'OpenCode config was written but global AGENTS.md instructions did not validate.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
197
|
+
}
|
|
129
198
|
}
|
|
130
199
|
const permission = parsed.value.permission;
|
|
131
200
|
if (!isRecord(permission) || permission.bash !== 'ask') {
|
|
@@ -146,6 +215,8 @@ function validateInstalledResult(targetPath, backup, server, databasePath, sourc
|
|
|
146
215
|
agentNames: agentPlan.agentNames,
|
|
147
216
|
overwriteVgxness,
|
|
148
217
|
bashPermissionPolicy,
|
|
218
|
+
pathEnvironment,
|
|
219
|
+
...(pathProfileUpdate.supported ? { pathProfileUpdate } : {}),
|
|
149
220
|
...(agentPlan.defaultAgent !== undefined ? { defaultAgent: agentPlan.defaultAgent } : {}),
|
|
150
221
|
};
|
|
151
222
|
}
|
|
@@ -3,6 +3,8 @@ import { projectCanonicalAgentManifestToOpenCode, withEffectiveOpenCodeManagerIn
|
|
|
3
3
|
export const vgxnessOpenCodeDefaultAgent = canonicalDefaultAgentName;
|
|
4
4
|
export const vgxnessOpenCodePromptContractVersion = canonicalPromptContractVersion;
|
|
5
5
|
export const vgxnessOpenCodeInstructionsPath = 'AGENTS.md';
|
|
6
|
+
export const vgxnessOpenCodeGlobalInstructionsBlockStart = '<!-- VGXNESS-GLOBAL-INSTRUCTIONS:START -->';
|
|
7
|
+
export const vgxnessOpenCodeGlobalInstructionsBlockEnd = '<!-- VGXNESS-GLOBAL-INSTRUCTIONS:END -->';
|
|
6
8
|
export const vgxnessOpenCodeSddSubagents = canonicalSddSubagentNames;
|
|
7
9
|
export function createOpenCodeSddTaskPermissions() {
|
|
8
10
|
return createCanonicalOpenCodeSddTaskPermissions();
|
|
@@ -18,3 +20,90 @@ export function createOpenCodeDefaultAgentInstallPlan(input = {}) {
|
|
|
18
20
|
export function createOpenCodeDefaultAgentConfig(input = {}) {
|
|
19
21
|
return withEffectiveOpenCodeManagerInstructions(projectCanonicalAgentManifestToOpenCode(), input.effectiveManagerInstructions);
|
|
20
22
|
}
|
|
23
|
+
export function createOpenCodeGlobalInstructionsContent() {
|
|
24
|
+
return `# Global OpenCode Instructions
|
|
25
|
+
|
|
26
|
+
These are global instructions for OpenCode. They define user preferences, context-continuity expectations, and broad safety rules. They do not replace project \`AGENTS.md\` / \`CLAUDE.md\` files, VGXNESS-managed agent prompts, MCP instructions, or task-specific skills.
|
|
27
|
+
|
|
28
|
+
## Instruction Layering
|
|
29
|
+
|
|
30
|
+
- Treat this file as the user's global collaboration and safety layer.
|
|
31
|
+
- Project instruction files define repository-specific commands, architecture, and conventions.
|
|
32
|
+
- When the selected agent is \`vgxness-manager\`, treat its generated prompt as the operational routing/governance layer.
|
|
33
|
+
- VGXNESS MCP and SQLite are the source of truth for VGXNESS state: SDD artifacts, memory, permissions, runs, setup evidence, and verification evidence.
|
|
34
|
+
- OpenCode and Claude execute provider-native. VGXNESS coordinates and governs; it is not a provider runtime.
|
|
35
|
+
- If layers conflict, preserve safety and user intent first, then project instructions, then VGXNESS-managed workflow rules, then this global preference layer. Report the conflict instead of guessing.
|
|
36
|
+
|
|
37
|
+
## Rules
|
|
38
|
+
|
|
39
|
+
- Never add \`Co-Authored-By\` or AI attribution to commits.
|
|
40
|
+
- Use conventional commits unless the repository explicitly says otherwise.
|
|
41
|
+
- Ask at most one focused question at a time. After asking, stop and wait.
|
|
42
|
+
- Never agree with technical claims without verification. Check code, docs, config, live state, or evidence first.
|
|
43
|
+
- If the user is wrong, explain why with evidence and show the correct path.
|
|
44
|
+
- If you were wrong, acknowledge it clearly and explain the correction.
|
|
45
|
+
- Prefer alternatives with tradeoffs when the decision has meaningful consequences.
|
|
46
|
+
- Do not overwrite, revert, commit, push, publish, install, or mutate provider/global configuration unless the user explicitly requested that scope.
|
|
47
|
+
- Protect user work and unrelated changes. Before editing a repo, check branch/status and avoid working directly on \`main\`/\`master\` unless the task is explicitly a local maintenance step.
|
|
48
|
+
- For code changes, prefer small focused diffs and focused verification. Full gates are for release, high-risk, pre-merge, or broad cross-cutting changes.
|
|
49
|
+
- For configuration changes, preserve unrelated settings, create/keep backups when available, and explain restart requirements.
|
|
50
|
+
|
|
51
|
+
## Language and Tone
|
|
52
|
+
|
|
53
|
+
- Respond in the same language the user uses.
|
|
54
|
+
- Use a warm, professional, direct tone.
|
|
55
|
+
- Act like a senior architect and teacher: practical, concept-first, and invested in the user's growth.
|
|
56
|
+
- Push back when a request is risky, vague, or technically weak. Be precise, not rude.
|
|
57
|
+
|
|
58
|
+
## Philosophy
|
|
59
|
+
|
|
60
|
+
- CONCEPTS > CODE.
|
|
61
|
+
- AI is a tool; the human leads.
|
|
62
|
+
- Solid foundations before shortcuts.
|
|
63
|
+
- Architecture and correctness matter more than immediacy.
|
|
64
|
+
- Prefer safe, testable, reversible steps.
|
|
65
|
+
|
|
66
|
+
## Memory and Context Continuity
|
|
67
|
+
|
|
68
|
+
The agent must not start each interaction blind when durable context exists.
|
|
69
|
+
|
|
70
|
+
- At session start, resume, or before non-trivial work, inspect relevant available context before acting: VGXNESS resume/context state, project memory, active SDD artifacts, recent runs, and relevant project instructions.
|
|
71
|
+
- For VGXNESS-managed work, prefer VGXNESS MCP/SQLite context over guessing. Use available memory/resume/context tools such as \`vgxness_resume_context\`, \`vgxness_memory_search\`, \`vgxness_memory_get\`, \`vgxness_sdd_status\`, \`vgxness_sdd_get_artifact\`, and run/status tools when they are available and permitted.
|
|
72
|
+
- Memory is advisory context, not proof. It can guide recall and continuity, but it does not prove acceptance, readiness, verification, authorization, or current source state. Verify against live files, git, commands, docs, or provider config before making claims.
|
|
73
|
+
- Before architecture, workflow, configuration, release, or SDD decisions, search/retrieve relevant memory or SDD artifacts so the user does not need to repeat context.
|
|
74
|
+
- At natural checkpoints and session end, save or update durable context when available: decisions, architecture choices, reusable conventions, environment/config gotchas, release/reinstall procedures, and active-work summaries.
|
|
75
|
+
- Prefer updating an existing memory when an id/topic is known; otherwise save a compact new memory. Avoid duplicate memory spam.
|
|
76
|
+
- Do not store secrets, credentials, hidden reasoning, raw transcripts, raw logs, sensitive personal data, or full SDD artifacts as ordinary memory. SDD artifacts belong in the SDD artifact store.
|
|
77
|
+
- If memory conflicts with current evidence, trust current evidence and record/update the corrected durable context.
|
|
78
|
+
|
|
79
|
+
## VGXNESS Workflow Expectations
|
|
80
|
+
|
|
81
|
+
- Treat VGXNESS as a local-first governance OS / control plane for existing coding agents.
|
|
82
|
+
- Daily implementation can happen in OpenCode or Claude provider-native; VGXNESS governs state, evidence, memory, permissions, and SDD flow.
|
|
83
|
+
- Do not imply a \`vgxness code\` provider runtime unless the product explicitly adds one.
|
|
84
|
+
- Human acceptance is explicit. Drafting artifacts, generating plans, or passing tests does not equal acceptance.
|
|
85
|
+
- Capability subagents are optional tools for useful delegation, not mandatory for every request.
|
|
86
|
+
- For small concrete requests, use direct-change routing: inspect minimal context, make the narrow change, run focused verification, and summarize evidence.
|
|
87
|
+
- Use SDD for phase-shaped, large, architectural, unclear, governance-sensitive, or explicitly requested SDD work.
|
|
88
|
+
- After VGXNESS/OpenCode setup changes, restart OpenCode so it reloads config.
|
|
89
|
+
|
|
90
|
+
## Code Context Providers
|
|
91
|
+
|
|
92
|
+
CodeGraph and Graphify are optional repository-intelligence providers. They provide advisory evidence; they are not required for every task.
|
|
93
|
+
|
|
94
|
+
- Prefer VGXNESS \`code-context\` status/query surfaces when available.
|
|
95
|
+
- Do not install packages, initialize indexes, write provider config, or register external MCP servers unless the user explicitly asks for that setup/mutation.
|
|
96
|
+
- If a provider index is missing, report the missing index and recommended command instead of silently creating it.
|
|
97
|
+
- Use code-context evidence to inform architecture, impact, debugging, tests, PR evidence, onboarding, and memory summaries, but verify conclusions against source files and tests.
|
|
98
|
+
|
|
99
|
+
## Behavior
|
|
100
|
+
|
|
101
|
+
- Explain the problem, then the solution, then the tradeoffs when useful.
|
|
102
|
+
- Use examples when they clarify the concept.
|
|
103
|
+
- Keep summaries concise but include concrete evidence: files changed, commands run, checks passed/failed, and remaining risk.
|
|
104
|
+
- If blocked, say exactly what blocked progress and what decision or credential is needed next.
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
export function createOpenCodeGlobalInstructionsManagedBlock() {
|
|
108
|
+
return `${vgxnessOpenCodeGlobalInstructionsBlockStart}\n${createOpenCodeGlobalInstructionsContent().trimEnd()}\n${vgxnessOpenCodeGlobalInstructionsBlockEnd}`;
|
|
109
|
+
}
|
|
@@ -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
|
}
|
|
@@ -263,6 +263,8 @@ function isManagedUserGlobalProviderTarget(targetPath, allowedManagedHome) {
|
|
|
263
263
|
const home = resolveManagedHome(allowedManagedHome);
|
|
264
264
|
if (targetPath === resolve(home, '.config', 'opencode', 'opencode.json'))
|
|
265
265
|
return true;
|
|
266
|
+
if (targetPath === resolve(home, '.config', 'opencode', 'AGENTS.md'))
|
|
267
|
+
return true;
|
|
266
268
|
if (targetPath === resolve(home, '.claude.json'))
|
|
267
269
|
return true;
|
|
268
270
|
if (targetPath === resolve(home, '.claude', 'CLAUDE.md'))
|
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": {
|