vgxness 1.9.8 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/cli/cli-flags.js +15 -2
- package/dist/cli/cli-help.js +13 -13
- package/dist/cli/commands/mcp-dispatcher.js +8 -4
- package/dist/cli/commands/setup-dispatcher.js +4 -4
- package/dist/cli/doctor-renderer.js +1 -1
- package/dist/cli/setup-plan-renderer.js +2 -2
- package/dist/cli/setup-status-renderer.js +2 -2
- package/dist/cli/tui/main-menu/main-menu-read-model.js +28 -28
- package/dist/cli/tui/main-menu/main-menu-render-shape.js +5 -1
- package/dist/cli/tui/opentui/main-menu/view.js +1 -1
- package/dist/cli/tui/setup/setup-tui-read-model.js +23 -23
- package/dist/cli/tui/setup/setup-tui-render-shape.js +15 -12
- package/dist/cli/tui/setup/setup-tui-state.js +1 -3
- package/dist/cli/tui/setup/setup-tui-view-helpers.js +18 -2
- package/dist/mcp/claude-code-config.js +2 -0
- package/dist/mcp/claude-code-scope.js +1 -0
- package/dist/mcp/claude-code-user-config.js +2 -0
- package/dist/mcp/client-install-claude-code-contract.js +15 -72
- package/dist/mcp/client-install-claude-code.js +4 -6
- package/dist/mcp/client-install-opencode-contract.js +5 -79
- package/dist/mcp/opencode-visibility.js +4 -3
- package/dist/mcp/provider-change-plan.js +17 -14
- package/dist/mcp/provider-doctor.js +38 -20
- package/dist/mcp/provider-health-types.js +19 -0
- package/dist/mcp/provider-status.js +29 -18
- package/dist/mcp/schema.js +5 -4
- package/dist/mcp/validation.js +11 -1
- package/dist/setup/backup-rollback-service.js +33 -2
- package/dist/setup/providers/claude-setup-adapter.js +13 -15
- package/dist/setup/providers/opencode-setup-adapter.js +12 -12
- package/dist/setup/setup-defaults.js +1 -0
- package/dist/setup/setup-plan.js +6 -6
- package/docs/architecture.md +3 -3
- package/docs/cli.md +29 -33
- package/docs/glossary.md +2 -2
- package/docs/mcp.md +1 -1
- package/docs/prd.md +3 -3
- package/docs/providers.md +14 -15
- package/docs/roadmap.md +1 -1
- package/docs/safety.md +1 -1
- package/docs/storage.md +1 -1
- package/package.json +1 -1
|
@@ -2,8 +2,8 @@ import { resolveMemoryDatabasePath } from '../memory/storage-paths.js';
|
|
|
2
2
|
import { planClaudeCodeMcpInstall } from './client-install-claude-code-contract.js';
|
|
3
3
|
import { planOpenCodeMcpInstall } from './client-install-opencode-contract.js';
|
|
4
4
|
import { ProviderDoctorService } from './provider-doctor.js';
|
|
5
|
+
import { providerRuntimeContext } from './provider-health-types.js';
|
|
5
6
|
import { ProviderStatusService } from './provider-status.js';
|
|
6
|
-
import { resolveClaudeCodeScope } from './claude-code-scope.js';
|
|
7
7
|
export const providerChangePlanProviders = ['opencode', 'claude', 'antigravity', 'custom'];
|
|
8
8
|
export const providerChangePlanTypes = ['opencode-mcp-install', 'claude-mcp-install', 'setup', 'install', 'config-preparation'];
|
|
9
9
|
const safety = {
|
|
@@ -76,13 +76,10 @@ export class ProviderChangePlanService {
|
|
|
76
76
|
const doctor = (this.deps.providerDoctor ?? new ProviderDoctorService()).getDoctor({ project: normalized.project, scope: normalized.scope, providerAdapter: 'claude', workspaceRoot: normalized.workspaceRoot, env: this.deps.env ?? process.env, payloadMode: normalized.payloadMode });
|
|
77
77
|
if (!doctor.ok)
|
|
78
78
|
return doctor;
|
|
79
|
-
const claudeScope = resolveClaudeCodeScope(normalized.scope);
|
|
80
|
-
if (!claudeScope.ok)
|
|
81
|
-
return claudeScope;
|
|
82
79
|
const databasePath = resolveMemoryDatabasePath({ cwd: normalized.workspaceRoot, env: this.deps.env ?? process.env });
|
|
83
80
|
if (!databasePath.ok)
|
|
84
81
|
return { ok: true, value: blockedPlanEnvelope(normalized, status.value, doctor.value, databasePath.error.message) };
|
|
85
|
-
const installPlan = planClaudeCodeMcpInstall({ cwd: normalized.workspaceRoot, databasePath: databasePath.value.path, databasePathSource: databasePath.value.source, scope:
|
|
82
|
+
const installPlan = planClaudeCodeMcpInstall({ cwd: normalized.workspaceRoot, databasePath: databasePath.value.path, databasePathSource: databasePath.value.source, scope: 'user', env: this.deps.env ?? process.env });
|
|
86
83
|
return { ok: true, value: claudeEnvelope(normalized, status.value, doctor.value, installPlan, databasePath.value.source) };
|
|
87
84
|
}
|
|
88
85
|
}
|
|
@@ -90,6 +87,7 @@ function normalizeInput(input) {
|
|
|
90
87
|
return {
|
|
91
88
|
project: input.project?.trim() || 'vgxness',
|
|
92
89
|
scope: input.scope ?? 'project',
|
|
90
|
+
installTarget: input.installTarget ?? 'user-global',
|
|
93
91
|
provider: input.provider,
|
|
94
92
|
changeType: input.changeType,
|
|
95
93
|
workspaceRoot: input.workspaceRoot.trim(),
|
|
@@ -102,7 +100,7 @@ function unsupportedProviderEnvelope(input) {
|
|
|
102
100
|
supported: false,
|
|
103
101
|
status: 'unsupported',
|
|
104
102
|
code: 'UNSUPPORTED_PROVIDER',
|
|
105
|
-
summary:
|
|
103
|
+
summary: `[read-only] ${input.provider} is a recognized provider value, but provider change planning currently supports OpenCode and Claude. No provider config was written. This diagnostic does not install or repair provider config.`,
|
|
106
104
|
providerIdentity: {
|
|
107
105
|
provider: input.provider,
|
|
108
106
|
adapter: input.provider,
|
|
@@ -116,7 +114,7 @@ function unsupportedProviderEnvelope(input) {
|
|
|
116
114
|
backupRollback: descriptiveBackupPolicy(false),
|
|
117
115
|
confirmations: confirmationPolicy(),
|
|
118
116
|
risks: ['No provider-specific change preview is available for this provider yet.'],
|
|
119
|
-
warnings: ['Known provider enum value is unsupported by this read-only planner.'],
|
|
117
|
+
warnings: ['Known provider enum value is unsupported by this read-only planner. No files were written.'],
|
|
120
118
|
};
|
|
121
119
|
}
|
|
122
120
|
function blockedPlanEnvelope(input, status, doctor, message) {
|
|
@@ -126,7 +124,7 @@ function blockedPlanEnvelope(input, status, doctor, message) {
|
|
|
126
124
|
supported: true,
|
|
127
125
|
status: 'blocked',
|
|
128
126
|
code: 'PLAN_UNAVAILABLE',
|
|
129
|
-
summary:
|
|
127
|
+
summary: `[read-only] ${providerName} change planning could not resolve the future MCP server command: ${message}. No provider config was written. This diagnostic does not install or repair provider config.`,
|
|
130
128
|
providerIdentity: {
|
|
131
129
|
provider: input.provider,
|
|
132
130
|
adapter: input.provider,
|
|
@@ -134,6 +132,7 @@ function blockedPlanEnvelope(input, status, doctor, message) {
|
|
|
134
132
|
},
|
|
135
133
|
statusSummary: statusSummary(status),
|
|
136
134
|
statusFindings: status.config,
|
|
135
|
+
configClassification: status.configClassification,
|
|
137
136
|
doctorSummary: doctorSummary(doctor),
|
|
138
137
|
doctorFindings: doctor.checks,
|
|
139
138
|
previewEffects: {
|
|
@@ -144,7 +143,7 @@ function blockedPlanEnvelope(input, status, doctor, message) {
|
|
|
144
143
|
backupRollback: descriptiveBackupPolicy(false),
|
|
145
144
|
confirmations: confirmationPolicy(),
|
|
146
145
|
risks: ['Future write planning requires a resolvable VGXNESS database path.'],
|
|
147
|
-
warnings: [message],
|
|
146
|
+
warnings: [message, 'No files were written.'],
|
|
148
147
|
};
|
|
149
148
|
}
|
|
150
149
|
function opencodeEnvelope(input, status, doctor, installPlan, source) {
|
|
@@ -162,6 +161,7 @@ function opencodeEnvelope(input, status, doctor, installPlan, source) {
|
|
|
162
161
|
},
|
|
163
162
|
statusSummary: statusSummary(status),
|
|
164
163
|
statusFindings: status.config,
|
|
164
|
+
configClassification: status.configClassification,
|
|
165
165
|
doctorSummary: doctorSummary(doctor),
|
|
166
166
|
doctorFindings: doctor.checks,
|
|
167
167
|
previewEffects: previewEffects(installPlan),
|
|
@@ -185,10 +185,11 @@ function claudeEnvelope(input, status, doctor, installPlan, source) {
|
|
|
185
185
|
supported: true,
|
|
186
186
|
status: installPlan.status === 'refused' ? 'blocked' : 'planned',
|
|
187
187
|
...(installPlan.status === 'refused' ? { code: 'PLAN_UNAVAILABLE' } : {}),
|
|
188
|
-
summary: installPlan.status === 'refused' ? `
|
|
188
|
+
summary: installPlan.status === 'refused' ? `[read-only] Claude ${input.changeType} planning completed; future install is currently refused: ${installPlan.message}. No provider config was written. This diagnostic does not install or repair provider config.` : '[read-only] Claude planning completed. No provider config was written. This diagnostic does not install or repair provider config. A future confirmed write would update ~/.claude.json, user agents, and ~/.claude/CLAUDE.md as needed. Project/local Claude files are diagnostics only.',
|
|
189
189
|
providerIdentity: { provider: 'claude', adapter: 'claude', support: 'supported' },
|
|
190
190
|
statusSummary: statusSummary(status),
|
|
191
191
|
statusFindings: status.config,
|
|
192
|
+
configClassification: status.configClassification,
|
|
192
193
|
doctorSummary: doctorSummary(doctor),
|
|
193
194
|
doctorFindings: doctor.checks,
|
|
194
195
|
previewEffects: previewEffectsClaude(installPlan),
|
|
@@ -205,11 +206,13 @@ function baseEnvelope(input) {
|
|
|
205
206
|
kind: 'provider-change-plan',
|
|
206
207
|
project: input.project,
|
|
207
208
|
scope: input.scope,
|
|
209
|
+
installTarget: input.installTarget,
|
|
208
210
|
provider: input.provider,
|
|
209
211
|
requestedChangeType: input.changeType,
|
|
210
212
|
normalizedChangeType: normalizeChangeType(input.provider, input.changeType),
|
|
211
213
|
payloadMode: input.payloadMode,
|
|
212
214
|
workspaceRoot: input.workspaceRoot,
|
|
215
|
+
runtimeContext: providerRuntimeContext(input),
|
|
213
216
|
safety,
|
|
214
217
|
};
|
|
215
218
|
}
|
|
@@ -304,7 +307,7 @@ function confirmationPolicy() {
|
|
|
304
307
|
requiredNow: false,
|
|
305
308
|
requiredBeforeFutureWrite: true,
|
|
306
309
|
acceptedMutationInputs: false,
|
|
307
|
-
message: '
|
|
310
|
+
message: '[read-only] No provider config was written. Any future provider config write must be requested and confirmed separately.',
|
|
308
311
|
};
|
|
309
312
|
}
|
|
310
313
|
function statusWarnings(status) {
|
|
@@ -323,7 +326,7 @@ function risksForOpenCode(status, doctor, plan, source) {
|
|
|
323
326
|
return risks;
|
|
324
327
|
}
|
|
325
328
|
function risksForClaude(status, doctor, plan, source) {
|
|
326
|
-
const risks = [
|
|
329
|
+
const risks = ['Claude user/global files affect this OS user; backups are required before merging existing files.', 'Claude Code runtime MCP approval happens in Claude Code; this plan does not prove runtime connection.'];
|
|
327
330
|
if (status.status !== 'ready')
|
|
328
331
|
risks.push(`Provider status is ${status.status}; future writes should review status findings first.`);
|
|
329
332
|
if (doctor.status !== 'healthy')
|
|
@@ -336,6 +339,6 @@ function risksForClaude(status, doctor, plan, source) {
|
|
|
336
339
|
}
|
|
337
340
|
function summaryForOpenCode(input, plan) {
|
|
338
341
|
if (plan.status === 'refused')
|
|
339
|
-
return `
|
|
340
|
-
return `
|
|
342
|
+
return `[read-only] OpenCode ${input.changeType} planning completed; future install is currently refused: ${plan.message}. No provider config was written. This diagnostic does not install or repair provider config.`;
|
|
343
|
+
return `[read-only] OpenCode ${input.changeType} planning completed. No provider config was written. This diagnostic does not install or repair provider config. A future confirmed write would ${plan.action} ${plan.targetPath}.`;
|
|
341
344
|
}
|
|
@@ -7,7 +7,7 @@ import { inspectClaudeCodeUserMcpConfig } from './claude-code-user-config.js';
|
|
|
7
7
|
import { inspectClaudeUserMemory } from './claude-code-user-memory.js';
|
|
8
8
|
import { vgxnessOpenCodeDefaultAgent, vgxnessOpenCodeSddSubagents } from './opencode-default-agent-config.js';
|
|
9
9
|
import { buildCanonicalAgentManifestDiagnostic } from './provider-canonical-agent-manifest.js';
|
|
10
|
-
import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, providerHealthFailure, isUserGlobalScope, REQUIRED_PROVIDER_NATIVE_MCP_TOOLS, rollupProviderDoctor, } from './provider-health-types.js';
|
|
10
|
+
import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, classifyProviderConfigPaths, providerHealthFailure, providerRuntimeContext, isUserGlobalScope, REQUIRED_PROVIDER_NATIVE_MCP_TOOLS, rollupProviderDoctor, } from './provider-health-types.js';
|
|
11
11
|
import { inspectOpenCodeConfigPaths } from './provider-status.js';
|
|
12
12
|
export class ProviderDoctorService {
|
|
13
13
|
deps;
|
|
@@ -65,6 +65,8 @@ export class ProviderDoctorService {
|
|
|
65
65
|
providerAdapter: normalized.providerAdapter,
|
|
66
66
|
scope: normalized.scope,
|
|
67
67
|
workspaceRoot: normalized.workspaceRoot,
|
|
68
|
+
runtimeContext: providerRuntimeContext(normalized),
|
|
69
|
+
configClassification: classifyProviderConfigPaths(paths),
|
|
68
70
|
status,
|
|
69
71
|
payloadMode: normalized.payloadMode,
|
|
70
72
|
overallStatus: status,
|
|
@@ -105,24 +107,24 @@ export class ProviderDoctorService {
|
|
|
105
107
|
const memoryFiles = advisoryPaths.filter((path) => existsSync(path));
|
|
106
108
|
const checks = [
|
|
107
109
|
{ id: 'workspace-root', status: existsSync(normalized.workspaceRoot) ? 'pass' : 'fail', detail: `Workspace root ${existsSync(normalized.workspaceRoot) ? 'exists' : 'does not exist'}: ${normalized.workspaceRoot}` },
|
|
108
|
-
{ id: 'provider-supported', status: 'pass', detail: 'Claude provider adapter selected
|
|
110
|
+
{ id: 'provider-supported', status: 'pass', detail: 'Claude provider adapter selected. VGX-managed Claude writes are user-global only; project Claude files are advisory external/manual diagnostics.' },
|
|
109
111
|
{ id: 'claude-scope', status: 'pass', detail: 'Claude scope project resolved to canonical project.' },
|
|
110
112
|
{ id: 'claude-cli-presence', status: 'warn', detail: 'Read-only doctor does not execute `claude --version`; CLI availability is diagnosed during explicit apply/preflight only.' },
|
|
111
|
-
{ id: 'claude-project-mcp-readable', status: mcp.status === 'invalid' ? '
|
|
112
|
-
{ id: 'claude-vgxness-mcp-entry', status: mcp.status === '
|
|
113
|
-
{ id: 'claude-agents-directory', status:
|
|
113
|
+
{ id: 'claude-project-mcp-readable', status: mcp.status === 'invalid' ? 'warn' : 'pass', detail: `${mcp.message} Advisory only; VGXNESS will not write project .mcp.json.`, ...(mcp.status === 'invalid' ? { remediation: 'Fix malformed project .mcp.json manually if Claude Code should read it.' } : {}) },
|
|
114
|
+
{ id: 'claude-vgxness-mcp-entry', status: mcp.status === 'conflicting' ? 'warn' : 'pass', detail: `${mcp.message} Advisory only; managed Claude MCP registration is user-global.`, ...(mcp.status === 'conflicting' ? { remediation: 'Manually reconcile project mcpServers.vgxness only if you intend to keep external project Claude config.' } : {}) },
|
|
115
|
+
{ id: 'claude-agents-directory', status: 'pass', detail: agents.directoryExists ? 'Claude project agents directory exists as external/manual diagnostics.' : 'Claude project agents directory is absent; VGXNESS will not create project-local Claude agents.' },
|
|
114
116
|
{
|
|
115
117
|
id: 'claude-vgxness-agents',
|
|
116
|
-
status: blockingAgents.length > 0 ? '
|
|
118
|
+
status: blockingAgents.length > 0 ? 'warn' : 'pass',
|
|
117
119
|
detail: blockingAgents.length > 0
|
|
118
|
-
? `
|
|
120
|
+
? `External/manual project Claude agents conflict or are invalid; VGXNESS will not repair them: ${blockingAgents.map((agent) => agent.agentName).join(', ')}.`
|
|
119
121
|
: missingAgents.length > 0
|
|
120
|
-
? `
|
|
121
|
-
: '
|
|
122
|
+
? `Project-local VGXNESS Claude agents are absent; they are no longer managed targets.`
|
|
123
|
+
: 'External/manual project Claude agent files were inspected.',
|
|
122
124
|
},
|
|
123
|
-
{ id: 'claude-agent-frontmatter', status: badFrontmatter.length === 0 ? 'pass' : '
|
|
124
|
-
{ id: 'claude-agent-generated-metadata', status: badMarkers.length === 0 ? 'pass' : '
|
|
125
|
-
|
|
125
|
+
{ id: 'claude-agent-frontmatter', status: badFrontmatter.length === 0 ? 'pass' : 'warn', detail: badFrontmatter.length === 0 ? 'No invalid project Claude agent frontmatter was detected.' : `External/manual project Claude agent frontmatter is invalid for: ${badFrontmatter.map((agent) => agent.agentName).join(', ')}.` },
|
|
126
|
+
{ id: 'claude-agent-generated-metadata', status: badMarkers.length === 0 ? 'pass' : 'warn', detail: badMarkers.length === 0 ? 'No project Claude agent metadata marker issues were detected.' : `External/manual project Claude agents are missing VGXNESS generated markers: ${badMarkers.map((agent) => agent.agentName).join(', ')}.` },
|
|
127
|
+
advisoryProjectMemoryCheck(projectMemory),
|
|
126
128
|
{ id: 'claude-project-memory-advisory', status: memoryFiles.length === 0 ? 'pass' : 'warn', detail: memoryFiles.length === 0 ? 'No Claude project memory/settings advisory files were found.' : `Advisory only; VGXNESS will not modify: ${memoryFiles.join(', ')}.` },
|
|
127
129
|
readonlySafetyCheck(before, snapshotPaths(checkedPathList, normalized.workspaceRoot)),
|
|
128
130
|
];
|
|
@@ -131,7 +133,7 @@ export class ProviderDoctorService {
|
|
|
131
133
|
const checkedPaths = normalized.payloadMode === 'verbose' ? checkedPathList : checkedPathList.filter((path) => existsSync(path) || path === mcp.path);
|
|
132
134
|
const failedChecks = checks.filter((check) => check.status === 'fail').map((check) => check.id);
|
|
133
135
|
const recommendations = checks.flatMap((check) => (check.remediation === undefined ? [] : [check.remediation]));
|
|
134
|
-
return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, status, payloadMode: normalized.payloadMode, overallStatus: status, checkCount: checks.length, passedCount: checks.filter((check) => check.status === 'pass').length, warningCount: checks.filter((check) => check.status === 'warn' || check.status === 'not-configured').length, errorCount: failedChecks.length, skippedCount: checks.filter((check) => check.status === 'skip').length, failedChecks, summary: summarizeDoctor(status, failedChecks.length, recommendations.length), recommendations, checks: compactChecksValue, checkedPaths, bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: checkedPathList }), 'utf8'), compactBytes: Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths }), 'utf8') }, verboseAvailable: normalized.payloadMode === 'compact', fullContentRef: `provider-doctor:claude:${normalized.workspaceRoot}`, generatedAt: 'read-only-snapshot', safety: PROVIDER_HEALTH_SAFETY } };
|
|
136
|
+
return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, runtimeContext: providerRuntimeContext(normalized), configClassification: externalProjectClassification(checkedPathList), status, payloadMode: normalized.payloadMode, overallStatus: status, checkCount: checks.length, passedCount: checks.filter((check) => check.status === 'pass').length, warningCount: checks.filter((check) => check.status === 'warn' || check.status === 'not-configured').length, errorCount: failedChecks.length, skippedCount: checks.filter((check) => check.status === 'skip').length, failedChecks, summary: summarizeDoctor(status, failedChecks.length, recommendations.length), recommendations, checks: compactChecksValue, checkedPaths, bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: checkedPathList }), 'utf8'), compactBytes: Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths }), 'utf8') }, verboseAvailable: normalized.payloadMode === 'compact', fullContentRef: `provider-doctor:claude:${normalized.workspaceRoot}`, generatedAt: 'read-only-snapshot', safety: PROVIDER_HEALTH_SAFETY } };
|
|
135
137
|
}
|
|
136
138
|
getClaudeUserGlobalDoctor(normalized, canonicalScope = 'user', scopeWarnings = []) {
|
|
137
139
|
const mcp = inspectClaudeCodeUserMcpConfig(normalized.env);
|
|
@@ -162,19 +164,26 @@ export class ProviderDoctorService {
|
|
|
162
164
|
const failedChecks = checks.filter((check) => check.status === 'fail').map((check) => check.id);
|
|
163
165
|
const recommendations = checks.flatMap((check) => (check.remediation === undefined ? [] : [check.remediation]));
|
|
164
166
|
const checkedPaths = normalized.payloadMode === 'verbose' ? checkedPathList : checkedPathList.filter((path) => existsSync(path) || path === mcp.path || path === userMemory.path);
|
|
165
|
-
return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, status, payloadMode: normalized.payloadMode, overallStatus: status, checkCount: checks.length, passedCount: checks.filter((check) => check.status === 'pass').length, warningCount: checks.filter((check) => check.status === 'warn' || check.status === 'not-configured').length, errorCount: failedChecks.length, skippedCount: checks.filter((check) => check.status === 'skip').length, failedChecks, summary: summarizeDoctor(status, failedChecks.length, recommendations.length), recommendations, checks: compactChecksValue, checkedPaths, bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: checkedPathList }), 'utf8'), compactBytes: Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths }), 'utf8') }, verboseAvailable: normalized.payloadMode === 'compact', fullContentRef: `provider-doctor:claude:${canonicalScope}:${normalized.workspaceRoot}`, generatedAt: 'read-only-snapshot', safety: { ...PROVIDER_HEALTH_SAFETY, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES } } };
|
|
167
|
+
return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, runtimeContext: providerRuntimeContext(normalized), configClassification: managedUserGlobalClassification(checkedPathList), status, payloadMode: normalized.payloadMode, overallStatus: status, checkCount: checks.length, passedCount: checks.filter((check) => check.status === 'pass').length, warningCount: checks.filter((check) => check.status === 'warn' || check.status === 'not-configured').length, errorCount: failedChecks.length, skippedCount: checks.filter((check) => check.status === 'skip').length, failedChecks, summary: summarizeDoctor(status, failedChecks.length, recommendations.length), recommendations, checks: compactChecksValue, checkedPaths, bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: checkedPathList }), 'utf8'), compactBytes: Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths }), 'utf8') }, verboseAvailable: normalized.payloadMode === 'compact', fullContentRef: `provider-doctor:claude:${canonicalScope}:${normalized.workspaceRoot}`, generatedAt: 'read-only-snapshot', safety: { ...PROVIDER_HEALTH_SAFETY, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES } } };
|
|
166
168
|
}
|
|
167
169
|
}
|
|
170
|
+
function externalProjectClassification(paths) {
|
|
171
|
+
return { managedUserGlobalConfig: [], detectedExternalProjectConfig: paths.filter((path) => existsSync(path)), detectedExternalUserConfig: [] };
|
|
172
|
+
}
|
|
173
|
+
function managedUserGlobalClassification(paths) {
|
|
174
|
+
return { managedUserGlobalConfig: [...paths], detectedExternalProjectConfig: [], detectedExternalUserConfig: [] };
|
|
175
|
+
}
|
|
168
176
|
function summarizeDoctor(status, failedCount, recommendationCount) {
|
|
177
|
+
const suffix = '[read-only] No provider config was written. This diagnostic does not install or repair provider config.';
|
|
169
178
|
if (status === 'healthy')
|
|
170
|
-
return
|
|
179
|
+
return `Provider doctor checks passed. ${suffix}`;
|
|
171
180
|
if (status === 'degraded')
|
|
172
|
-
return `Provider doctor found warnings and ${recommendationCount} recommendation(s)
|
|
181
|
+
return `Provider doctor found warnings and ${recommendationCount} recommendation(s). ${suffix}`;
|
|
173
182
|
if (status === 'failed')
|
|
174
|
-
return `Provider doctor found ${failedCount} failed check(s)
|
|
183
|
+
return `Provider doctor found ${failedCount} failed check(s). ${suffix}`;
|
|
175
184
|
if (status === 'skipped')
|
|
176
|
-
return
|
|
177
|
-
return
|
|
185
|
+
return `Provider doctor checks were skipped. ${suffix}`;
|
|
186
|
+
return `Provider doctor status is unknown; review checks before continuing. ${suffix}`;
|
|
178
187
|
}
|
|
179
188
|
function compactChecks(checks, mode) {
|
|
180
189
|
if (mode === 'verbose')
|
|
@@ -272,7 +281,7 @@ function readonlySafetyCheck(before, after) {
|
|
|
272
281
|
? {
|
|
273
282
|
id: 'provider-config-readonly-safety',
|
|
274
283
|
status: 'pass',
|
|
275
|
-
detail: 'Provider config path existence and mtimes were unchanged
|
|
284
|
+
detail: '[read-only] Provider config path existence and mtimes were unchanged. No files were written. This diagnostic does not install or repair provider config.',
|
|
276
285
|
}
|
|
277
286
|
: { id: 'provider-config-readonly-safety', status: 'fail', detail: 'Provider config path existence or mtimes changed during read-only doctor.' };
|
|
278
287
|
}
|
|
@@ -289,6 +298,15 @@ function claudeProjectMemoryCheck(state) {
|
|
|
289
298
|
: { remediation: 'Run confirmed Claude project install when ready.' }),
|
|
290
299
|
};
|
|
291
300
|
}
|
|
301
|
+
function advisoryProjectMemoryCheck(state) {
|
|
302
|
+
const status = state.status === 'blocked' || state.status === 'managed-stale' ? 'warn' : 'pass';
|
|
303
|
+
return {
|
|
304
|
+
id: 'claude-project-memory-managed-block',
|
|
305
|
+
status,
|
|
306
|
+
detail: `${state.message} Advisory only; VGXNESS will not write project-root CLAUDE.md.`,
|
|
307
|
+
...(status === 'warn' ? { remediation: 'Manually reconcile project-root CLAUDE.md only if you intend to keep external project Claude memory.' } : {}),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
292
310
|
function snapshotPaths(paths, workspaceRoot) {
|
|
293
311
|
const unique = [...paths, `${workspaceRoot}/.vgx`];
|
|
294
312
|
return Object.fromEntries(unique.map((path) => [path, existsSync(path) ? statSync(path).mtimeMs : false]));
|
|
@@ -12,9 +12,28 @@ export const PROVIDER_HEALTH_SAFETY = {
|
|
|
12
12
|
executesProvider: false,
|
|
13
13
|
};
|
|
14
14
|
export const CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES = ['status', 'doctor', 'change-plan'];
|
|
15
|
+
export function providerConfigPathDiagnostics(ownership, exists) {
|
|
16
|
+
return {
|
|
17
|
+
ownership,
|
|
18
|
+
managed: ownership === 'managed-user-global',
|
|
19
|
+
readOnlyDiagnostic: true,
|
|
20
|
+
mayAffectProviderBehaviorExternally: ownership.startsWith('external-') && exists,
|
|
21
|
+
...(ownership === 'managed-user-global' ? { installTarget: 'user-global' } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
15
24
|
export function isUserGlobalScope(scope) {
|
|
16
25
|
return scope === 'personal' || scope === 'user';
|
|
17
26
|
}
|
|
27
|
+
export function providerRuntimeContext(input) {
|
|
28
|
+
return { project: input.project, workspaceRoot: input.workspaceRoot, diagnosticScope: input.scope };
|
|
29
|
+
}
|
|
30
|
+
export function classifyProviderConfigPaths(paths) {
|
|
31
|
+
return {
|
|
32
|
+
managedUserGlobalConfig: paths.filter((path) => path.diagnostics.ownership === 'managed-user-global').map((path) => path.path),
|
|
33
|
+
detectedExternalProjectConfig: paths.filter((path) => path.diagnostics.ownership === 'external-project' && path.exists).map((path) => path.path),
|
|
34
|
+
detectedExternalUserConfig: paths.filter((path) => path.diagnostics.ownership === 'external-user' && path.exists).map((path) => path.path),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
18
37
|
export const REQUIRED_PROVIDER_MCP_TOOLS = ['vgxness_provider_status', 'vgxness_provider_doctor', 'vgxness_provider_change_plan'];
|
|
19
38
|
export const REQUIRED_PROVIDER_NATIVE_MCP_TOOLS = [
|
|
20
39
|
'vgxness_provider_status',
|
|
@@ -9,7 +9,7 @@ import { inspectClaudeUserMemory } from './claude-code-user-memory.js';
|
|
|
9
9
|
import { resolveOpenCodeMcpInstallTarget } from './client-install-opencode-contract.js';
|
|
10
10
|
import { vgxnessOpenCodeDefaultAgent, vgxnessOpenCodePromptContractVersion, vgxnessOpenCodeSddSubagents } from './opencode-default-agent-config.js';
|
|
11
11
|
import { buildCanonicalAgentManifestDiagnostic } from './provider-canonical-agent-manifest.js';
|
|
12
|
-
import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, providerHealthFailure, isUserGlobalScope, REQUIRED_PROVIDER_MCP_TOOLS, rollupProviderHealth, } from './provider-health-types.js';
|
|
12
|
+
import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, classifyProviderConfigPaths, providerConfigPathDiagnostics, providerRuntimeContext, providerHealthFailure, isUserGlobalScope, REQUIRED_PROVIDER_MCP_TOOLS, rollupProviderHealth, } from './provider-health-types.js';
|
|
13
13
|
const projectConfigTargets = ['.opencode/opencode.json', 'opencode.json', '.opencode/opencode.jsonc', 'opencode.jsonc'];
|
|
14
14
|
export class ProviderStatusService {
|
|
15
15
|
deps;
|
|
@@ -59,6 +59,8 @@ export class ProviderStatusService {
|
|
|
59
59
|
providerAdapter: normalized.providerAdapter,
|
|
60
60
|
scope: normalized.scope,
|
|
61
61
|
workspaceRoot: normalized.workspaceRoot,
|
|
62
|
+
runtimeContext: providerRuntimeContext(normalized),
|
|
63
|
+
configClassification: classifyProviderConfigPaths(paths),
|
|
62
64
|
status,
|
|
63
65
|
overallStatus: status,
|
|
64
66
|
inspectedPaths: checkedPaths,
|
|
@@ -123,6 +125,8 @@ export class ProviderStatusService {
|
|
|
123
125
|
providerAdapter: 'claude',
|
|
124
126
|
scope: normalized.scope,
|
|
125
127
|
workspaceRoot: normalized.workspaceRoot,
|
|
128
|
+
runtimeContext: providerRuntimeContext(normalized),
|
|
129
|
+
configClassification: classifyProviderConfigPaths(paths),
|
|
126
130
|
status,
|
|
127
131
|
overallStatus: status,
|
|
128
132
|
inspectedPaths: checkedPaths,
|
|
@@ -153,7 +157,7 @@ export class ProviderStatusService {
|
|
|
153
157
|
const mcpState = inspectClaudeCodeUserMcpConfig(normalized.env);
|
|
154
158
|
const agents = inspectClaudeCodeAgents({ workspaceRoot: normalized.workspaceRoot, scope: 'user', env: normalized.env });
|
|
155
159
|
const userMemory = inspectClaudeUserMemory(normalized.env);
|
|
156
|
-
const userMemoryPathStatus = { ...claudeProjectMemoryPathStatus(userMemory), label: 'user ~/.claude/CLAUDE.md managed block' };
|
|
160
|
+
const userMemoryPathStatus = { ...claudeProjectMemoryPathStatus(userMemory, 'managed-user-global'), label: 'user ~/.claude/CLAUDE.md managed block' };
|
|
157
161
|
const paths = [claudeUserMcpConfigPathStatus(mcpState), userMemoryPathStatus];
|
|
158
162
|
const mcpEntry = claudeUserMcpEntryStatus(mcpState);
|
|
159
163
|
const agentStatuses = agents.agents.map((agent) => (agent.status === 'managed' ? 'pass' : agent.status === 'missing' ? 'not-configured' : 'fail'));
|
|
@@ -173,6 +177,8 @@ export class ProviderStatusService {
|
|
|
173
177
|
providerAdapter: 'claude',
|
|
174
178
|
scope: normalized.scope,
|
|
175
179
|
workspaceRoot: normalized.workspaceRoot,
|
|
180
|
+
runtimeContext: providerRuntimeContext(normalized),
|
|
181
|
+
configClassification: classifyProviderConfigPaths(paths),
|
|
176
182
|
status,
|
|
177
183
|
overallStatus: status,
|
|
178
184
|
inspectedPaths: checkedPaths,
|
|
@@ -211,7 +217,7 @@ function claudeConfigHealthStatus(statuses) {
|
|
|
211
217
|
return 'warn';
|
|
212
218
|
return 'pass';
|
|
213
219
|
}
|
|
214
|
-
function claudeProjectMemoryPathStatus(state) {
|
|
220
|
+
function claudeProjectMemoryPathStatus(state, ownership = 'external-project') {
|
|
215
221
|
const status = state.status === 'managed-current' ? 'pass' : state.status === 'managed-stale' ? 'warn' : state.status === 'blocked' ? 'fail' : 'not-configured';
|
|
216
222
|
return {
|
|
217
223
|
label: 'project CLAUDE.md managed block',
|
|
@@ -221,12 +227,13 @@ function claudeProjectMemoryPathStatus(state) {
|
|
|
221
227
|
parsed: state.status === 'managed-current' || state.status === 'managed-stale',
|
|
222
228
|
status,
|
|
223
229
|
detail: state.message,
|
|
230
|
+
diagnostics: providerConfigPathDiagnostics(ownership, state.exists),
|
|
224
231
|
};
|
|
225
232
|
}
|
|
226
233
|
function compactPaths(paths, mode) {
|
|
227
234
|
if (mode === 'verbose')
|
|
228
235
|
return paths;
|
|
229
|
-
return paths.map((path) => ({ label: path.label, path: path.path, status: path.status }));
|
|
236
|
+
return paths.map((path) => ({ label: path.label, path: path.path, status: path.status, diagnostics: path.diagnostics }));
|
|
230
237
|
}
|
|
231
238
|
function compactMcpEntry(entry, mode) {
|
|
232
239
|
if (mode === 'verbose')
|
|
@@ -260,12 +267,12 @@ function readFirstParsedConfig(paths) {
|
|
|
260
267
|
}
|
|
261
268
|
}
|
|
262
269
|
export function inspectProjectConfigPaths(workspaceRoot) {
|
|
263
|
-
return projectConfigTargets.map((relativePath) => inspectConfigPath(relativePath, join(workspaceRoot, relativePath)));
|
|
270
|
+
return projectConfigTargets.map((relativePath) => inspectConfigPath(relativePath, join(workspaceRoot, relativePath), 'external-project'));
|
|
264
271
|
}
|
|
265
272
|
export function inspectOpenCodeConfigPaths(workspaceRoot, env = process.env) {
|
|
266
273
|
const userTarget = resolveOpenCodeMcpInstallTarget({ cwd: workspaceRoot, scope: 'user', env });
|
|
267
274
|
const userPaths = userTarget.ok
|
|
268
|
-
? [inspectConfigPath('user ~/.config/opencode/opencode.json', userTarget.path)]
|
|
275
|
+
? [inspectConfigPath('user ~/.config/opencode/opencode.json', userTarget.path, 'managed-user-global')]
|
|
269
276
|
: [
|
|
270
277
|
{
|
|
271
278
|
label: 'user ~/.config/opencode/opencode.json',
|
|
@@ -275,13 +282,14 @@ export function inspectOpenCodeConfigPaths(workspaceRoot, env = process.env) {
|
|
|
275
282
|
parsed: false,
|
|
276
283
|
status: 'warn',
|
|
277
284
|
detail: userTarget.message,
|
|
285
|
+
diagnostics: providerConfigPathDiagnostics('managed-user-global', false),
|
|
278
286
|
},
|
|
279
287
|
];
|
|
280
288
|
return [...userPaths, ...inspectProjectConfigPaths(workspaceRoot)];
|
|
281
289
|
}
|
|
282
|
-
function inspectConfigPath(label, path) {
|
|
290
|
+
function inspectConfigPath(label, path, ownership = 'not-managed') {
|
|
283
291
|
if (!existsSync(path))
|
|
284
|
-
return { label, path, exists: false, readable: false, parsed: false, status: 'not-configured', detail: 'Config path does not exist.' };
|
|
292
|
+
return { label, path, exists: false, readable: false, parsed: false, status: 'not-configured', detail: 'Config path does not exist.', diagnostics: providerConfigPathDiagnostics(ownership, false) };
|
|
285
293
|
if (label.endsWith('.jsonc'))
|
|
286
294
|
return {
|
|
287
295
|
label,
|
|
@@ -291,14 +299,15 @@ function inspectConfigPath(label, path) {
|
|
|
291
299
|
parsed: false,
|
|
292
300
|
status: 'warn',
|
|
293
301
|
detail: 'JSONC config exists but is not parsed by this read-only status check.',
|
|
302
|
+
diagnostics: providerConfigPathDiagnostics(ownership, true),
|
|
294
303
|
};
|
|
295
304
|
try {
|
|
296
305
|
JSON.parse(readFileSync(path, 'utf8'));
|
|
297
|
-
return { label, path, exists: true, readable: true, parsed: true, status: 'pass', detail: 'Config exists and parses as JSON.' };
|
|
306
|
+
return { label, path, exists: true, readable: true, parsed: true, status: 'pass', detail: 'Config exists and parses as JSON.', diagnostics: providerConfigPathDiagnostics(ownership, true) };
|
|
298
307
|
}
|
|
299
308
|
catch (cause) {
|
|
300
309
|
const message = cause instanceof Error ? cause.message : String(cause);
|
|
301
|
-
return { label, path, exists: true, readable: false, parsed: false, status: 'fail', detail: `Config could not be read or parsed: ${message}
|
|
310
|
+
return { label, path, exists: true, readable: false, parsed: false, status: 'fail', detail: `Config could not be read or parsed: ${message}`, diagnostics: providerConfigPathDiagnostics(ownership, true) };
|
|
302
311
|
}
|
|
303
312
|
}
|
|
304
313
|
function inspectOpenCodeMcpEntry(paths) {
|
|
@@ -345,22 +354,24 @@ function hasProjectConfigAlongsideUser(paths) {
|
|
|
345
354
|
return paths.some((path) => path.label.startsWith('user ') && path.exists) && paths.some((path) => !path.label.startsWith('user ') && path.exists);
|
|
346
355
|
}
|
|
347
356
|
function summarizeStatus(status, mcpEntry) {
|
|
357
|
+
const suffix = '[read-only] No provider config was written. This diagnostic does not install or repair provider config.';
|
|
348
358
|
if (status === 'ready')
|
|
349
|
-
return
|
|
359
|
+
return `OpenCode provider config is readable and the vgxness MCP entry is present. ${suffix}`;
|
|
350
360
|
if (status === 'not-configured')
|
|
351
|
-
return mcpEntry.detail
|
|
361
|
+
return `${mcpEntry.detail} ${suffix}`;
|
|
352
362
|
if (status === 'warning')
|
|
353
|
-
return
|
|
354
|
-
return
|
|
363
|
+
return `OpenCode provider config has warnings. ${suffix}`;
|
|
364
|
+
return `OpenCode provider status check found a blocking issue. ${suffix}`;
|
|
355
365
|
}
|
|
356
366
|
function summarizeClaudeStatus(status, mcpEntry) {
|
|
367
|
+
const suffix = '[read-only] No provider config was written. This diagnostic does not install or repair provider config.';
|
|
357
368
|
if (status === 'ready')
|
|
358
|
-
return
|
|
369
|
+
return `Claude provider files were inspected and VGXNESS entries are readable. ${suffix}`;
|
|
359
370
|
if (status === 'not-configured')
|
|
360
|
-
return mcpEntry.detail
|
|
371
|
+
return `${mcpEntry.detail} ${suffix}`;
|
|
361
372
|
if (status === 'warning')
|
|
362
|
-
return
|
|
363
|
-
return
|
|
373
|
+
return `Claude provider project config has warnings. ${suffix}`;
|
|
374
|
+
return `Claude provider status check found a blocking issue. ${suffix}`;
|
|
364
375
|
}
|
|
365
376
|
function nextActionFor(status, mcpEntry, sddNext) {
|
|
366
377
|
if (sddNext !== undefined)
|
package/dist/mcp/schema.js
CHANGED
|
@@ -97,7 +97,7 @@ export function toInternalVgxMcpToolName(toolName) {
|
|
|
97
97
|
return EXPOSED_TO_INTERNAL_TOOL_NAMES[toolName];
|
|
98
98
|
}
|
|
99
99
|
const scopes = ['project', 'personal'];
|
|
100
|
-
const
|
|
100
|
+
const providerInstallTargets = ['user-global'];
|
|
101
101
|
const agentModes = ['agent', 'subagent'];
|
|
102
102
|
const memoryTypes = ['architecture', 'decision', 'bugfix', 'pattern', 'config', 'discovery', 'learning', 'preference', 'manual'];
|
|
103
103
|
const activityKinds = ['prompt', 'tool_call', 'artifact', 'summary', 'error'];
|
|
@@ -469,7 +469,7 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
|
|
|
469
469
|
vgxness_provider_status: z
|
|
470
470
|
.object({
|
|
471
471
|
project: z.string().min(1).optional(),
|
|
472
|
-
scope: z.enum(
|
|
472
|
+
scope: z.enum(scopes).optional(),
|
|
473
473
|
providerAdapter: z.enum(['opencode', 'claude']).optional(),
|
|
474
474
|
workspaceRoot: z.string().min(1).optional(),
|
|
475
475
|
change: z.string().min(1).optional(),
|
|
@@ -479,7 +479,7 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
|
|
|
479
479
|
vgxness_provider_doctor: z
|
|
480
480
|
.object({
|
|
481
481
|
project: z.string().min(1).optional(),
|
|
482
|
-
scope: z.enum(
|
|
482
|
+
scope: z.enum(scopes).optional(),
|
|
483
483
|
providerAdapter: z.enum(['opencode', 'claude']).optional(),
|
|
484
484
|
workspaceRoot: z.string().min(1).optional(),
|
|
485
485
|
expectedPromptContractVersion: z.number().int().positive().optional(),
|
|
@@ -489,7 +489,8 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
|
|
|
489
489
|
vgxness_provider_change_plan: z
|
|
490
490
|
.object({
|
|
491
491
|
project: z.string().min(1).optional(),
|
|
492
|
-
scope: z.enum(
|
|
492
|
+
scope: z.enum(scopes).optional(),
|
|
493
|
+
installTarget: z.enum(providerInstallTargets).optional(),
|
|
493
494
|
provider: z.enum(providerChangePlanProviders),
|
|
494
495
|
changeType: z.enum(providerChangePlanTypes),
|
|
495
496
|
workspaceRoot: z.string().min(1),
|
package/dist/mcp/validation.js
CHANGED
|
@@ -16,6 +16,8 @@ const payloadModes = ['compact', 'verbose'];
|
|
|
16
16
|
const contextCockpitLevels = ['compact', 'expanded', 'verbose'];
|
|
17
17
|
const providerChangePlanProviders = ['opencode', 'claude', 'antigravity', 'custom'];
|
|
18
18
|
const providerChangePlanTypes = ['opencode-mcp-install', 'claude-mcp-install', 'setup', 'install', 'config-preparation'];
|
|
19
|
+
const providerInstallTargets = ['user-global'];
|
|
20
|
+
const providerUserGlobalOnlyMigrationMessage = 'VGX-managed provider configuration is user-global only for OpenCode and Claude. Project/local provider files are treated as external/manual diagnostics and will not be written by VGXNESS. Use installTarget: user-global.';
|
|
19
21
|
const validChangePattern = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
20
22
|
const validMemoryTopicKeyPattern = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
21
23
|
const maxMemoryTitleLength = 200;
|
|
@@ -139,7 +141,7 @@ function readVerificationChangeType(record, tool) {
|
|
|
139
141
|
return { ok: true, value: trimmed };
|
|
140
142
|
}
|
|
141
143
|
function validateProviderChangePlanInput(input, tool) {
|
|
142
|
-
const record = inputRecord(input, tool, ['project', 'scope', 'provider', 'changeType', 'workspaceRoot', 'payloadMode']);
|
|
144
|
+
const record = inputRecord(input, tool, ['project', 'scope', 'installTarget', 'provider', 'changeType', 'workspaceRoot', 'payloadMode']);
|
|
143
145
|
if (!record.ok)
|
|
144
146
|
return record;
|
|
145
147
|
const provider = readRequiredOneOf(record.value, 'provider', providerChangePlanProviders, tool);
|
|
@@ -170,6 +172,14 @@ function validateProviderChangePlanInput(input, tool) {
|
|
|
170
172
|
return scope;
|
|
171
173
|
if (scope.value !== undefined)
|
|
172
174
|
result.scope = scope.value;
|
|
175
|
+
if (typeof record.value.installTarget === 'string' && record.value.installTarget.trim() !== 'user-global') {
|
|
176
|
+
return validationFailure(providerUserGlobalOnlyMigrationMessage, tool);
|
|
177
|
+
}
|
|
178
|
+
const installTarget = readOptionalOneOf(record.value, 'installTarget', providerInstallTargets, tool);
|
|
179
|
+
if (!installTarget.ok)
|
|
180
|
+
return installTarget;
|
|
181
|
+
if (installTarget.value !== undefined)
|
|
182
|
+
result.installTarget = installTarget.value;
|
|
173
183
|
return { ok: true, value: result };
|
|
174
184
|
}
|
|
175
185
|
function validateProviderHealthInput(input, tool) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import {
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
5
|
export function createConfigBackupPath(targetPath, label = 'backup') {
|
|
5
6
|
return `${targetPath}.${label}-${new Date().toISOString().replaceAll(/[-:.]/g, '')}`;
|
|
6
7
|
}
|
|
@@ -202,12 +203,18 @@ function classifyBackup(backupPath) {
|
|
|
202
203
|
return managedSummary(backupPath, metadataPath, parsed.value, validation.blockers, validation.warnings);
|
|
203
204
|
}
|
|
204
205
|
const target = inferRollbackTarget(backupPath);
|
|
206
|
+
const blockers = target.ok ? [] : [target.error.message];
|
|
207
|
+
if (target.ok) {
|
|
208
|
+
const rollbackTargetBlocker = validateProviderRollbackTarget(target.value);
|
|
209
|
+
if (rollbackTargetBlocker !== undefined)
|
|
210
|
+
blockers.push(rollbackTargetBlocker);
|
|
211
|
+
}
|
|
205
212
|
return {
|
|
206
213
|
kind: 'legacy',
|
|
207
214
|
backupPath,
|
|
208
215
|
provider: 'opencode',
|
|
209
216
|
...(target.ok ? { targetPath: target.value } : {}),
|
|
210
|
-
blockers
|
|
217
|
+
blockers,
|
|
211
218
|
warnings: ['Legacy backup has no VGXNESS sidecar metadata; target is inferred from the filename.'],
|
|
212
219
|
};
|
|
213
220
|
}
|
|
@@ -238,8 +245,32 @@ function validateMetadataBinding(backupPath, metadata) {
|
|
|
238
245
|
}
|
|
239
246
|
else
|
|
240
247
|
blockers.push(`Backup file does not exist: ${backupPath}`);
|
|
248
|
+
const rollbackTargetBlocker = validateProviderRollbackTarget(metadata.target.path);
|
|
249
|
+
if (rollbackTargetBlocker !== undefined)
|
|
250
|
+
blockers.push(rollbackTargetBlocker);
|
|
241
251
|
return { blockers, warnings };
|
|
242
252
|
}
|
|
253
|
+
function validateProviderRollbackTarget(targetPath) {
|
|
254
|
+
const normalizedTarget = resolve(targetPath);
|
|
255
|
+
if (isManagedUserGlobalProviderTarget(normalizedTarget))
|
|
256
|
+
return undefined;
|
|
257
|
+
return `Refusing to restore provider config backup to ${normalizedTarget}. VGX-managed provider rollback writes user-global OpenCode/Claude targets only; project-local provider files are external/manual diagnostics and will not be restored.`;
|
|
258
|
+
}
|
|
259
|
+
function isManagedUserGlobalProviderTarget(targetPath) {
|
|
260
|
+
const home = resolve(homedir());
|
|
261
|
+
if (targetPath === resolve(home, '.config', 'opencode', 'opencode.json'))
|
|
262
|
+
return true;
|
|
263
|
+
if (targetPath === resolve(home, '.claude.json'))
|
|
264
|
+
return true;
|
|
265
|
+
if (targetPath === resolve(home, '.claude', 'CLAUDE.md'))
|
|
266
|
+
return true;
|
|
267
|
+
const claudeAgentsRoot = resolve(home, '.claude', 'agents');
|
|
268
|
+
return isPathInside(claudeAgentsRoot, targetPath) && dirname(targetPath) === claudeAgentsRoot && basename(targetPath).endsWith('.md');
|
|
269
|
+
}
|
|
270
|
+
function isPathInside(parentPath, candidatePath) {
|
|
271
|
+
const relation = relative(parentPath, candidatePath);
|
|
272
|
+
return relation === '' || (relation !== '' && !relation.startsWith('..') && !isAbsolute(relation));
|
|
273
|
+
}
|
|
243
274
|
function managedSummary(backupPath, metadataPath, metadata, blockers, warnings) {
|
|
244
275
|
return {
|
|
245
276
|
kind: 'managed',
|